Initial release: export 1C Help to Gitea wiki.
Convert Ext/Help/ru.html to Markdown and push to a wiki git repository.
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
out/
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
.pytest_cache/
|
||||
.mypy_cache/
|
||||
.DS_Store
|
||||
*.egg-info/
|
||||
.venv/
|
||||
@@ -0,0 +1,23 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to **export_1c_help** are documented in this file.
|
||||
|
||||
## [0.1.0] - 2026-07-23
|
||||
|
||||
### Status
|
||||
|
||||
**In development.** Full dump of configuration Help to Gitea wiki.
|
||||
|
||||
### Added
|
||||
|
||||
- Discovery of `**/Ext/Help/ru.html` in a 1C configuration `src/` tree
|
||||
- HTML → Markdown converter (stdlib only): headings, lists, bold/italic, links
|
||||
- Rewrite of 1C help links (`Catalog.X/Help`, `….Form.Y/Help`) to Gitea `[[wiki]]` links
|
||||
- Unique wiki page titles (H1 / synonym / meta key) with collision suffixes
|
||||
- Gitea-compatible percent-encoded filenames for Cyrillic titles
|
||||
- `Home.md` index and `_Sidebar.md` navigation
|
||||
- `manifest.json` (meta_key → title mapping)
|
||||
- CLI: `convert`, `push` (clone/commit/push wiki git repo)
|
||||
- Option `--objects-only` to skip form-level help
|
||||
|
||||
[0.1.0]: https://git.p7net.ru/tools/export_1c_help/releases/tag/v0.1.0
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 RHANA
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,100 @@
|
||||
# export_1c_help
|
||||
|
||||
**Версия:** см. [`VERSION`](VERSION) (текущая: **0.1.0**) · [CHANGELOG](CHANGELOG.md)
|
||||
**Лицензия:** [MIT](LICENSE)
|
||||
|
||||
Выгрузка встроенной справки конфигурации 1С (`**/Ext/Help/ru.html`) в Markdown и публикация в [Gitea Wiki](https://docs.gitea.com/usage/wiki).
|
||||
|
||||
Зависимости: **Python 3.10+**, только стандартная библиотека.
|
||||
|
||||
---
|
||||
|
||||
## Быстрый старт
|
||||
|
||||
```bash
|
||||
cd tools/export_1c_help
|
||||
|
||||
# конвертация в каталог
|
||||
python3 export_1c_help.py convert \
|
||||
-s ../../crm3-26/src \
|
||||
-o ./out/wiki-md \
|
||||
--clean
|
||||
|
||||
# конвертация + push в wiki-репозиторий Gitea
|
||||
python3 export_1c_help.py push \
|
||||
-s ../../crm3-26/src \
|
||||
--wiki-url https://git.p7net.ru/1c/crm3_26.wiki.git \
|
||||
-o ./out/wiki-md
|
||||
```
|
||||
|
||||
Проверка версии:
|
||||
|
||||
```bash
|
||||
python3 export_1c_help.py --version
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Команды
|
||||
|
||||
### `convert`
|
||||
|
||||
Читает `src/`, пишет Markdown-дерево wiki:
|
||||
|
||||
| Файл | Назначение |
|
||||
|------|------------|
|
||||
| `Home.md` | оглавление по типам метаданных |
|
||||
| `_Sidebar.md` | боковая навигация (объекты без форм) |
|
||||
| `«Заголовок».md` / `%D0%….md` | страницы справки (кириллица — percent-encoded, как в Gitea) |
|
||||
| `manifest.json` | карта `meta_key` → title / filename |
|
||||
|
||||
| Параметр | Описание |
|
||||
|----------|----------|
|
||||
| `-s` / `--src` | каталог `src` выгрузки конфигурации |
|
||||
| `-o` / `--out` | каталог результата |
|
||||
| `--objects-only` | без справки форм (`Forms/*/Ext/Help`) |
|
||||
| `--keep-empty` | не пропускать почти пустые страницы |
|
||||
| `--clean` | очистить `--out` перед записью (кроме `.git`) |
|
||||
|
||||
### `push`
|
||||
|
||||
Выполняет `convert`, затем `git clone` / обновление wiki-репозитория (обычно `*.wiki.git`), коммит и `git push`.
|
||||
|
||||
| Параметр | Описание |
|
||||
|----------|----------|
|
||||
| `--wiki-url` | URL wiki git, напр. `https://git.p7net.ru/1c/crm3_26.wiki.git` |
|
||||
| `--work-dir` | локальный clone wiki |
|
||||
| `--dry-run` | только convert |
|
||||
| `--keep-unmanaged` | не удалять чужие `.md` в wiki |
|
||||
| `-m` / `--message` | сообщение коммита |
|
||||
|
||||
Для `push` нужны права записи в wiki (HTTPS + credential helper / token).
|
||||
|
||||
---
|
||||
|
||||
## Как устроены ссылки
|
||||
|
||||
В HTML справки 1С:
|
||||
|
||||
```text
|
||||
Catalog.ВидыНоменклатуры/Help
|
||||
Document.ЗаказКлиента.Form.ФормаДокумента/Help
|
||||
```
|
||||
|
||||
В wiki:
|
||||
|
||||
```markdown
|
||||
[[Виды номенклатуры]]
|
||||
[[текст|Виды номенклатуры]]
|
||||
```
|
||||
|
||||
Ключ метаданных (`Catalog.ВидыНоменклатуры`) стабилен и пишется в подвал страницы и в `manifest.json`.
|
||||
|
||||
---
|
||||
|
||||
## Репозиторий
|
||||
|
||||
Отдельный git-репозиторий (подмодуль монорепозитория миграции):
|
||||
|
||||
- путь в дереве проекта: `tools/export_1c_help/`
|
||||
- remote (ожидаемый): `https://git.p7net.ru/tools/export_1c_help.git`
|
||||
@@ -0,0 +1,7 @@
|
||||
"""Export 1C configuration Help (ru.html) to Markdown / Gitea wiki."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from export1c_help.__version__ import __status__, __version__
|
||||
|
||||
__all__ = ["__version__", "__status__"]
|
||||
@@ -0,0 +1,18 @@
|
||||
"""export_1c_help version."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
_VERSION_FILE = Path(__file__).resolve().parent.parent / "VERSION"
|
||||
|
||||
|
||||
def _read_version() -> str:
|
||||
try:
|
||||
return _VERSION_FILE.read_text(encoding="utf-8").strip()
|
||||
except OSError:
|
||||
return "0.0.0"
|
||||
|
||||
|
||||
__version__ = _read_version()
|
||||
__status__ = "in development"
|
||||
@@ -0,0 +1,279 @@
|
||||
"""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""
|
||||
|
||||
return text
|
||||
@@ -0,0 +1,234 @@
|
||||
"""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"<Synonym>\s*<v8:item>\s*<v8:lang>ru</v8:lang>\s*<v8:content>(.*?)</v8:content>",
|
||||
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
|
||||
@@ -0,0 +1,199 @@
|
||||
"""Build markdown wiki pages from discovered Help HTML."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import shutil
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
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.naming import assign_titles
|
||||
from export1c_help.types_map import DIR_TO_RU
|
||||
from export1c_help.__version__ import __version__
|
||||
|
||||
|
||||
@dataclass
|
||||
class ExportStats:
|
||||
pages_total: int = 0
|
||||
pages_written: int = 0
|
||||
pages_skipped_empty: int = 0
|
||||
images_copied: int = 0
|
||||
|
||||
|
||||
def _is_effectively_empty(md: str) -> bool:
|
||||
plain = re_sub_md(md)
|
||||
return len(plain) < 20
|
||||
|
||||
|
||||
def re_sub_md(md: str) -> str:
|
||||
import re
|
||||
|
||||
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 build_home(pages: list[HelpPage], titles: dict[str, str], *, source_label: str) -> str:
|
||||
by_type: dict[str, list[HelpPage]] = {}
|
||||
for p in pages:
|
||||
by_type.setdefault(p.type_dir, []).append(p)
|
||||
|
||||
lines = [
|
||||
f"# Справка конфигурации",
|
||||
"",
|
||||
f"Автогенерация из встроенной справки 1С (`Ext/Help/ru.html`).",
|
||||
"",
|
||||
f"- Источник: `{source_label}`",
|
||||
f"- Страниц: **{len(pages)}**",
|
||||
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|Оглавление]]", ""]
|
||||
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("")
|
||||
# only object-level in sidebar to keep it usable
|
||||
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 export_help(
|
||||
src_root: Path,
|
||||
out_dir: Path,
|
||||
*,
|
||||
include_forms: bool = True,
|
||||
skip_empty: bool = True,
|
||||
clean: bool = False,
|
||||
source_label: 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()
|
||||
|
||||
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)
|
||||
|
||||
manifest_pages: list[dict] = []
|
||||
|
||||
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
|
||||
|
||||
# footer
|
||||
md = (
|
||||
md.rstrip()
|
||||
+ "\n\n---\n\n"
|
||||
+ f"*Мета: `{p.meta_key}` · файл: `{p.rel_posix}`*\n"
|
||||
)
|
||||
|
||||
fname = files[p.meta_key]
|
||||
(out_dir / fname).write_text(md, encoding="utf-8")
|
||||
stats.pages_written += 1
|
||||
|
||||
# copy sibling images
|
||||
help_dir = p.path.parent
|
||||
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
|
||||
|
||||
manifest_pages.append(
|
||||
{
|
||||
"meta_key": p.meta_key,
|
||||
"title": title,
|
||||
"filename": fname,
|
||||
"type_dir": p.type_dir,
|
||||
"object": p.object_name,
|
||||
"form": p.form_name,
|
||||
"source": p.rel_posix,
|
||||
}
|
||||
)
|
||||
|
||||
label = source_label or str(src_root)
|
||||
written_keys = {m["meta_key"] for m in manifest_pages}
|
||||
written_pages = [p for p in pages if p.meta_key in written_keys]
|
||||
(out_dir / "Home.md").write_text(
|
||||
build_home(written_pages, titles, source_label=label),
|
||||
encoding="utf-8",
|
||||
)
|
||||
(out_dir / "_Sidebar.md").write_text(
|
||||
build_sidebar(written_pages, titles),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
manifest = {
|
||||
"tool": "export_1c_help",
|
||||
"version": __version__,
|
||||
"source": label,
|
||||
"exported_at": datetime.now(timezone.utc).isoformat(),
|
||||
"pages_total": stats.pages_total,
|
||||
"pages_written": stats.pages_written,
|
||||
"pages_skipped_empty": stats.pages_skipped_empty,
|
||||
"pages": manifest_pages,
|
||||
}
|
||||
(out_dir / "manifest.json").write_text(
|
||||
json.dumps(manifest, ensure_ascii=False, indent=2) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
return stats
|
||||
@@ -0,0 +1,106 @@
|
||||
"""Wiki page titles and Gitea filename encoding."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import re
|
||||
from urllib.parse import quote, unquote
|
||||
|
||||
from export1c_help.discover import HelpPage
|
||||
|
||||
_INVALID = re.compile(r'[\\/:*?"<>|#\[\]]+')
|
||||
_SPACES = re.compile(r"\s+")
|
||||
_MAX_NAME_BYTES = 180
|
||||
|
||||
|
||||
def sanitize_title(title: str) -> str:
|
||||
title = _SPACES.sub(" ", title).strip()
|
||||
title = _INVALID.sub(" ", title)
|
||||
title = _SPACES.sub(" ", title).strip()
|
||||
return title or "untitled"
|
||||
|
||||
|
||||
def _truncate_utf8(s: str, max_bytes: int) -> str:
|
||||
raw = s.encode("utf-8")
|
||||
if len(raw) <= max_bytes:
|
||||
return s
|
||||
raw = raw[:max_bytes]
|
||||
while raw:
|
||||
try:
|
||||
return raw.decode("utf-8").rstrip("- ")
|
||||
except UnicodeDecodeError:
|
||||
raw = raw[:-1]
|
||||
return "page"
|
||||
|
||||
|
||||
def _short_hash(s: str) -> str:
|
||||
return hashlib.sha1(s.encode("utf-8")).hexdigest()[:8]
|
||||
|
||||
|
||||
def wiki_filename(title: str, *, meta_key: str | None = None) -> str:
|
||||
"""Unicode wiki slug (spaces → «-»)."""
|
||||
slug = sanitize_title(title).replace(" ", "-")
|
||||
# reserve room for optional -xxxxxxxx suffix
|
||||
slug = _truncate_utf8(slug, _MAX_NAME_BYTES)
|
||||
return f"{slug}.md"
|
||||
|
||||
|
||||
def wiki_filename_gitea_legacy(title: str) -> str:
|
||||
slug = sanitize_title(title).replace(" ", "-")
|
||||
slug = _truncate_utf8(slug, 80)
|
||||
return quote(slug, safe="-_.") + ".md"
|
||||
|
||||
|
||||
def decode_wiki_filename(name: str) -> str:
|
||||
base = name[:-3] if name.endswith(".md") else name
|
||||
return unquote(base)
|
||||
|
||||
|
||||
def assign_titles(
|
||||
pages: list[HelpPage],
|
||||
h1_by_meta: dict[str, str | None],
|
||||
) -> tuple[dict[str, str], dict[str, str]]:
|
||||
"""
|
||||
Returns (meta_key → title, meta_key → filename).
|
||||
|
||||
Filenames are unique even after UTF-8 truncation.
|
||||
"""
|
||||
used_title: set[str] = set()
|
||||
used_file: set[str] = set()
|
||||
titles: dict[str, str] = {}
|
||||
files: dict[str, str] = {}
|
||||
|
||||
for p in pages:
|
||||
h1 = h1_by_meta.get(p.meta_key)
|
||||
base = sanitize_title(h1 or p.synonym or p.meta_key)
|
||||
title = base
|
||||
n = 2
|
||||
while title.casefold() in used_title:
|
||||
if p.is_form and p.form_name and n == 2:
|
||||
title = sanitize_title(f"{base} — {p.form_name}")
|
||||
elif n == 2:
|
||||
title = sanitize_title(f"{base} — {p.object_name}")
|
||||
else:
|
||||
title = sanitize_title(f"{base} ({n})")
|
||||
n += 1
|
||||
if n > 5000:
|
||||
title = sanitize_title(p.meta_key)
|
||||
break
|
||||
|
||||
fname = wiki_filename(title)
|
||||
if fname.casefold() in used_file:
|
||||
stem = fname[:-3]
|
||||
stem = _truncate_utf8(stem, _MAX_NAME_BYTES - 9)
|
||||
fname = f"{stem}-{_short_hash(p.meta_key)}.md"
|
||||
# last-resort unique
|
||||
guard = 0
|
||||
while fname.casefold() in used_file:
|
||||
fname = f"page-{_short_hash(p.meta_key + str(guard))}.md"
|
||||
guard += 1
|
||||
|
||||
used_title.add(title.casefold())
|
||||
used_file.add(fname.casefold())
|
||||
titles[p.meta_key] = title
|
||||
files[p.meta_key] = fname
|
||||
|
||||
return titles, files
|
||||
@@ -0,0 +1,73 @@
|
||||
"""Mapping between 1C export folders and help link type prefixes."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
# Folder under src/ → type used in v8 help links (Catalog.X/Help)
|
||||
DIR_TO_LINK_TYPE: dict[str, str] = {
|
||||
"Catalogs": "Catalog",
|
||||
"Documents": "Document",
|
||||
"DocumentJournals": "DocumentJournal",
|
||||
"Enums": "Enum",
|
||||
"Reports": "Report",
|
||||
"DataProcessors": "DataProcessor",
|
||||
"InformationRegisters": "InformationRegister",
|
||||
"AccumulationRegisters": "AccumulationRegister",
|
||||
"AccountingRegisters": "AccountingRegister",
|
||||
"CalculationRegisters": "CalculationRegister",
|
||||
"ChartsOfCharacteristicTypes": "ChartOfCharacteristicTypes",
|
||||
"ChartsOfAccounts": "ChartOfAccounts",
|
||||
"ChartsOfCalculationTypes": "ChartOfCalculationTypes",
|
||||
"BusinessProcesses": "BusinessProcess",
|
||||
"Tasks": "Task",
|
||||
"ExchangePlans": "ExchangePlan",
|
||||
"FilterCriteria": "FilterCriterion",
|
||||
"SettingsStorages": "SettingsStorage",
|
||||
"CommonForms": "CommonForm",
|
||||
"CommonCommands": "CommonCommand",
|
||||
"CommonModules": "CommonModule",
|
||||
"CommonTemplates": "CommonTemplate",
|
||||
"CommonPictures": "CommonPicture",
|
||||
"XDTOPackages": "XDTOPackage",
|
||||
"WebServices": "WebService",
|
||||
"HTTPServices": "HTTPService",
|
||||
"WSReferences": "WSReference",
|
||||
"Styles": "Style",
|
||||
"Languages": "Language",
|
||||
"FunctionalOptions": "FunctionalOption",
|
||||
"FunctionalOptionsParameters": "FunctionalOptionsParameter",
|
||||
"DefinedTypes": "DefinedType",
|
||||
"SessionParameters": "SessionParameter",
|
||||
"Constants": "Constant",
|
||||
"Sequences": "Sequence",
|
||||
"Subsystems": "Subsystem",
|
||||
"Roles": "Role",
|
||||
"Interfaces": "Interface",
|
||||
"EventSubscriptions": "EventSubscription",
|
||||
"ScheduledJobs": "ScheduledJob",
|
||||
}
|
||||
|
||||
DIR_TO_RU: dict[str, str] = {
|
||||
"Catalogs": "Справочники",
|
||||
"Documents": "Документы",
|
||||
"DocumentJournals": "Журналы документов",
|
||||
"Enums": "Перечисления",
|
||||
"Reports": "Отчёты",
|
||||
"DataProcessors": "Обработки",
|
||||
"InformationRegisters": "Регистры сведений",
|
||||
"AccumulationRegisters": "Регистры накопления",
|
||||
"AccountingRegisters": "Регистры бухгалтерии",
|
||||
"CalculationRegisters": "Регистры расчёта",
|
||||
"ChartsOfCharacteristicTypes": "Планы видов характеристик",
|
||||
"ChartsOfAccounts": "Планы счетов",
|
||||
"ChartsOfCalculationTypes": "Планы видов расчёта",
|
||||
"BusinessProcesses": "Бизнес-процессы",
|
||||
"Tasks": "Задачи",
|
||||
"ExchangePlans": "Планы обмена",
|
||||
"FilterCriteria": "Критерии отбора",
|
||||
"SettingsStorages": "Хранилища настроек",
|
||||
"CommonForms": "Общие формы",
|
||||
"CommonCommands": "Общие команды",
|
||||
"Subsystems": "Подсистемы",
|
||||
}
|
||||
|
||||
LINK_TYPE_TO_DIR: dict[str, str] = {v: k for k, v in DIR_TO_LINK_TYPE.items()}
|
||||
@@ -0,0 +1,93 @@
|
||||
"""Push generated wiki tree into a Gitea wiki git repository."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class WikiPushError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
def _run(cmd: list[str], *, cwd: Path) -> str:
|
||||
proc = subprocess.run(
|
||||
cmd,
|
||||
cwd=cwd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
if proc.returncode != 0:
|
||||
raise WikiPushError(
|
||||
f"$ {' '.join(cmd)}\n{proc.stdout}\n{proc.stderr}".strip()
|
||||
)
|
||||
return proc.stdout
|
||||
|
||||
|
||||
def push_wiki(
|
||||
content_dir: Path,
|
||||
wiki_url: str,
|
||||
*,
|
||||
work_dir: Path,
|
||||
message: str,
|
||||
branch: str = "main",
|
||||
keep_unmanaged: bool = False,
|
||||
) -> None:
|
||||
"""
|
||||
Clone/fetch wiki repo into work_dir, replace managed pages, commit, push.
|
||||
|
||||
Managed pages = all *.md from content_dir plus images and manifest.json.
|
||||
If keep_unmanaged=False, removes other *.md (except maybe custom) before copy.
|
||||
"""
|
||||
content_dir = content_dir.resolve()
|
||||
work_dir = work_dir.resolve()
|
||||
|
||||
if work_dir.exists() and (work_dir / ".git").exists():
|
||||
_run(["git", "fetch", "origin"], cwd=work_dir)
|
||||
_run(["git", "checkout", branch], cwd=work_dir)
|
||||
_run(["git", "reset", "--hard", f"origin/{branch}"], cwd=work_dir)
|
||||
else:
|
||||
if work_dir.exists():
|
||||
shutil.rmtree(work_dir)
|
||||
work_dir.parent.mkdir(parents=True, exist_ok=True)
|
||||
_run(
|
||||
["git", "clone", "--branch", branch, "--single-branch", wiki_url, str(work_dir)],
|
||||
cwd=work_dir.parent,
|
||||
)
|
||||
|
||||
if not keep_unmanaged:
|
||||
for path in work_dir.iterdir():
|
||||
if path.name == ".git":
|
||||
continue
|
||||
if path.is_file() and path.suffix.lower() in {".md", ".json"}:
|
||||
path.unlink()
|
||||
elif path.is_file() and path.suffix.lower() in {
|
||||
".png",
|
||||
".jpg",
|
||||
".jpeg",
|
||||
".gif",
|
||||
".bmp",
|
||||
".webp",
|
||||
}:
|
||||
path.unlink()
|
||||
|
||||
for src in content_dir.iterdir():
|
||||
if src.name == ".git":
|
||||
continue
|
||||
dst = work_dir / src.name
|
||||
if src.is_dir():
|
||||
if dst.exists():
|
||||
shutil.rmtree(dst)
|
||||
shutil.copytree(src, dst)
|
||||
else:
|
||||
shutil.copy2(src, dst)
|
||||
|
||||
_run(["git", "add", "-A"], cwd=work_dir)
|
||||
status = _run(["git", "status", "--porcelain"], cwd=work_dir)
|
||||
if not status.strip():
|
||||
return
|
||||
|
||||
_run(["git", "commit", "-m", message], cwd=work_dir)
|
||||
_run(["git", "push", "origin", f"HEAD:{branch}"], cwd=work_dir)
|
||||
@@ -0,0 +1,176 @@
|
||||
#!/usr/bin/env python3
|
||||
"""CLI: export 1C Help (ru.html) to Markdown / Gitea wiki."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
TOOL_ROOT = Path(__file__).resolve().parent
|
||||
if str(TOOL_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(TOOL_ROOT))
|
||||
|
||||
from export1c_help import __status__, __version__ # noqa: E402
|
||||
from export1c_help.export import export_help # noqa: E402
|
||||
from export1c_help.wiki import WikiPushError, push_wiki # noqa: E402
|
||||
|
||||
|
||||
def build_parser() -> argparse.ArgumentParser:
|
||||
p = argparse.ArgumentParser(
|
||||
prog="export_1c_help",
|
||||
description=(
|
||||
f"Выгрузка встроенной справки 1С (Ext/Help/ru.html) в Markdown / Gitea wiki. "
|
||||
f"v{__version__} ({__status__})."
|
||||
),
|
||||
)
|
||||
p.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
|
||||
|
||||
sub = p.add_subparsers(dest="cmd", required=True)
|
||||
|
||||
c = sub.add_parser("convert", help="Конвертация Help → Markdown в каталог")
|
||||
c.add_argument(
|
||||
"-s",
|
||||
"--src",
|
||||
type=Path,
|
||||
required=True,
|
||||
help="Каталог src выгрузки конфигурации (…/crm3-26/src)",
|
||||
)
|
||||
c.add_argument(
|
||||
"-o",
|
||||
"--out",
|
||||
type=Path,
|
||||
required=True,
|
||||
help="Каталог результата (wiki working tree)",
|
||||
)
|
||||
c.add_argument(
|
||||
"--objects-only",
|
||||
action="store_true",
|
||||
help="Только справка объектов (без Forms/*/Ext/Help)",
|
||||
)
|
||||
c.add_argument(
|
||||
"--keep-empty",
|
||||
action="store_true",
|
||||
help="Не пропускать почти пустые страницы",
|
||||
)
|
||||
c.add_argument(
|
||||
"--clean",
|
||||
action="store_true",
|
||||
help="Очистить --out перед записью (кроме .git)",
|
||||
)
|
||||
c.add_argument(
|
||||
"--source-label",
|
||||
default=None,
|
||||
help="Подпись источника в Home.md",
|
||||
)
|
||||
c.set_defaults(func=cmd_convert)
|
||||
|
||||
w = sub.add_parser("push", help="convert + push в Gitea wiki (.wiki.git)")
|
||||
w.add_argument("-s", "--src", type=Path, required=True)
|
||||
w.add_argument(
|
||||
"--wiki-url",
|
||||
required=True,
|
||||
help="URL wiki-репозитория, напр. https://git.p7net.ru/1c/crm3_26.wiki.git",
|
||||
)
|
||||
w.add_argument(
|
||||
"--work-dir",
|
||||
type=Path,
|
||||
default=None,
|
||||
help="Локальный clone wiki (по умолчанию: <out>/.wiki_clone)",
|
||||
)
|
||||
w.add_argument(
|
||||
"-o",
|
||||
"--out",
|
||||
type=Path,
|
||||
default=None,
|
||||
help="Промежуточный каталог MD (по умолчанию: ./out/wiki-md)",
|
||||
)
|
||||
w.add_argument("--objects-only", action="store_true")
|
||||
w.add_argument("--keep-empty", action="store_true")
|
||||
w.add_argument(
|
||||
"--keep-unmanaged",
|
||||
action="store_true",
|
||||
help="Не удалять чужие .md в wiki перед копированием",
|
||||
)
|
||||
w.add_argument(
|
||||
"-m",
|
||||
"--message",
|
||||
default=None,
|
||||
help="Сообщение коммита wiki",
|
||||
)
|
||||
w.add_argument("--branch", default="main")
|
||||
w.add_argument(
|
||||
"--dry-run",
|
||||
action="store_true",
|
||||
help="Только convert, без git push",
|
||||
)
|
||||
w.set_defaults(func=cmd_push)
|
||||
|
||||
return p
|
||||
|
||||
|
||||
def cmd_convert(args: argparse.Namespace) -> int:
|
||||
stats = export_help(
|
||||
args.src,
|
||||
args.out,
|
||||
include_forms=not args.objects_only,
|
||||
skip_empty=not args.keep_empty,
|
||||
clean=args.clean,
|
||||
source_label=args.source_label,
|
||||
)
|
||||
print(
|
||||
f"OK: written={stats.pages_written} "
|
||||
f"skipped_empty={stats.pages_skipped_empty} "
|
||||
f"total={stats.pages_total} "
|
||||
f"images={stats.images_copied} → {args.out}"
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_push(args: argparse.Namespace) -> int:
|
||||
out = args.out or (TOOL_ROOT / "out" / "wiki-md")
|
||||
work = args.work_dir or (out.parent / "wiki_clone")
|
||||
stats = export_help(
|
||||
args.src,
|
||||
out,
|
||||
include_forms=not args.objects_only,
|
||||
skip_empty=not args.keep_empty,
|
||||
clean=True,
|
||||
source_label=str(args.src),
|
||||
)
|
||||
print(
|
||||
f"convert: written={stats.pages_written} "
|
||||
f"skipped_empty={stats.pages_skipped_empty} "
|
||||
f"total={stats.pages_total} → {out}"
|
||||
)
|
||||
if args.dry_run:
|
||||
print("dry-run: push skipped")
|
||||
return 0
|
||||
|
||||
message = args.message or (
|
||||
f"export_1c_help v{__version__}: sync help ({stats.pages_written} pages)"
|
||||
)
|
||||
try:
|
||||
push_wiki(
|
||||
out,
|
||||
args.wiki_url,
|
||||
work_dir=work,
|
||||
message=message,
|
||||
branch=args.branch,
|
||||
keep_unmanaged=args.keep_unmanaged,
|
||||
)
|
||||
except WikiPushError as exc:
|
||||
print(f"push failed:\n{exc}", file=sys.stderr)
|
||||
return 1
|
||||
print(f"OK: pushed → {args.wiki_url}")
|
||||
return 0
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
parser = build_parser()
|
||||
args = parser.parse_args(argv)
|
||||
return args.func(args)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1 @@
|
||||
# Python stdlib only (3.10+)
|
||||
Reference in New Issue
Block a user