From fd3af9795727aa949a91298eccdf77c0161cba32 Mon Sep 17 00:00:00 2001 From: Vladimir Nadulich Date: Tue, 21 Oct 2025 22:33:34 +0300 Subject: [PATCH] Init --- .gitignore | 7 + Readme.md | 0 onec_codetemplate_parser/__init__.py | 0 onec_codetemplate_parser/main.py | 327 +++++++++++++++++++++++++++ pyproject.toml | 7 + setup.py | 7 + tests/common.py | 16 ++ tests/conftest.py | 30 +++ tests/test_01.py | 45 ++++ 9 files changed, 439 insertions(+) create mode 100644 .gitignore create mode 100644 Readme.md create mode 100644 onec_codetemplate_parser/__init__.py create mode 100644 onec_codetemplate_parser/main.py create mode 100644 pyproject.toml create mode 100644 setup.py create mode 100644 tests/common.py create mode 100644 tests/conftest.py create mode 100644 tests/test_01.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e3bd67f --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +temp/* +.vscode/* + +__pycache__ +*.egg-info + +.coverage diff --git a/Readme.md b/Readme.md new file mode 100644 index 0000000..e69de29 diff --git a/onec_codetemplate_parser/__init__.py b/onec_codetemplate_parser/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/onec_codetemplate_parser/main.py b/onec_codetemplate_parser/main.py new file mode 100644 index 0000000..9610d0c --- /dev/null +++ b/onec_codetemplate_parser/main.py @@ -0,0 +1,327 @@ +import sys, os, shutil +import re +from typing import List, Union + +class Node: + def __init__(self, name: str): + self.name = name + self.parent = None + self.path = None + self.position = None + + def set_parent(self, parent): + self.parent = parent + self.position = parent.children.index(self)+1 + +class Leaf(Node): + """Обычный лист с пятью полями""" + def __init__(self, name: str, menu_flag: int, replace: str, text: str): + super().__init__(name) + self.menu_flag = int(menu_flag) + self.replace = replace + self.text = text + + def __repr__(self): + return f"Leaf({self.name!r}, menu={self.menu_flag}, '{self.replace}')" + + def pretty_print(self, indent=0): + pad = " " * indent + print(f"{pad}* Leaf: {self.name} (key: {self.replace})") + + def compile(self) -> str: + parts = [ + "{0,\n{", + f'"{self.name}"', + ",0,", + str(self.menu_flag), + ',"', + self.replace, + '","', + self.text, + '"}\n}', + ] + return "".join(parts) + + def to_files(self): + + data = [ + {"Название": self.name}, + {"ВключатьВКонтекстноеМеню": self.menu_flag}, + {"АвтоматическиЗаменятьСтроку": self.replace}, + {"Текст": self.text.replace('""', '"')}, + ] + + safe_name = re.sub(r'[\\/*?:"<>|]', "_", self.name) + self.path = f"{self.parent.path}/{self.position:03d}.0_{safe_name}.yml" + + with open(self.path, 'w', encoding='utf-8') as f: + for item in data: + #нужно добавить \n между записями, кроме первой + if f.tell() > 0: + f.write("\n") + for key, value in item.items(): + f.write(f"[{key}]\n") + f.write(f"{value}") + + @classmethod + def from_files(cls, path): + + with open(path, 'r', encoding='utf-8') as f: + lines = f.readlines() + name = lines[1].strip() + menu_flag = int(lines[3].strip()) + replace = lines[5].strip() + text = ''.join(lines[7:]).replace('"', '""') + leaf = Leaf(name, menu_flag, replace, text) + + return leaf + +class Group(Node): + """Группа: заголовок + список подэлементов (листов/групп)""" + def __init__(self, counter: int, name: str, children: List[Union["Group", Leaf]]): + super().__init__(name) + self.counter = int(counter) + self.children = children + for child in self.children: + child.set_parent(self) + + def __repr__(self): + return f"Group({self.name!r}, {len(self.children)} children)" + + def pretty_print(self, indent=0): + pad = " " * indent + print(f"{pad}- Group: {self.name}") + for child in self.children: + child.pretty_print(indent + 2) + + def compile(self) -> str: + parts = [ + '{', + str(self.counter), + ',\n{', + f'"{self.name}"', + ',1,0,"",""}', + ] + for child in self.children: + parts.append(",\n") + parts.append(child.compile()) + parts.append('\n}') + return "".join(parts) + + def to_files(self): + safe_name = re.sub(r'[\\/*?:"<>|]', "_", self.name) + self.path = f"{self.parent.path}/{self.position:03d}.0_{safe_name}" + + assert not os.path.isfile(self.path), f"Путь '{self.path}' является файлом, а не директорией" + if not os.path.exists(self.path): + os.makedirs(self.path) + + for child in self.children: + child.to_files() + + @classmethod + def from_files(cls, path): + + entries = sorted(os.listdir(path)) + children = [] + for entry in entries: + full_path = os.path.join(path, entry) + if os.path.isdir(full_path): + child = Group.from_files(full_path) + children.append(child) + else: + #лист + children.append(Leaf.from_files(full_path)) + #создать группу + group_name = re.sub(r'^.+?_', '', os.path.basename(path)) + return Group(len(children), group_name, children) + +class Root(Node): + def __init__(self, counter: int, children: List[Union[Group, Leaf]]): + self.counter = int(counter) + self.children = children + for child in self.children: + child.set_parent(self) + + def __repr__(self): + return f"Root({len(self.children)} children)" + + def pretty_print(self, indent=0): + pad = " " * indent + print(f"{pad}Root:") + for child in self.children: + child.pretty_print(indent + 2) + print("") + + def compile(self) -> str: + parts = [ "{", str(self.counter) ] + for child in self.children: + parts.append(",\n") + parts.append(child.compile()) + parts.append("\n}") + return "".join(parts) + + def to_files(self, path): + self.path = path + + assert not os.path.isfile(path), f"Путь '{path}' является файлом, а не директорией" + if os.path.exists(path): + shutil.rmtree(path) + os.makedirs(path) + + for child in self.children: + child.to_files() + + @classmethod + def from_files(cls, path): + + assert not os.path.isfile(path), f"Путь '{path}' является файлом, а не директорией" + assert os.path.exists(path), f"Дирректория '{path}' не существует" + + #прочитать все файлы и собрать обратно в дерево + entries = sorted(os.listdir(path)) + children = [] + for entry in entries: + full_path = os.path.join(path, entry) + if os.path.isdir(full_path): + child = Group.from_files(full_path) + children.append(child) + return Root(len(children), children) + +def parse_skobkofile(text: str) -> Root: + pos = 0 + + def skip_ws(): + nonlocal pos + while pos < len(text) and text[pos] in " \n\r\t": + pos += 1 + + def take(s: str): + nonlocal pos + skip_ws() + length = len(s) + assert text[pos:pos+length] == s, f"Ожидалось '{s}' на позиции {pos}" + pos += length + skip_ws() + + def parse_value(): + nonlocal pos + if text[pos] == '"': + return string_value() + else: + return numeric_value() + + def string_value(): + nonlocal pos + pos += 1 + start = pos + while True: + if text[pos] != '"': + pos += 1 + elif text[pos:pos+2] == '""': + pos += 2 + else: + break + s = text[start:pos] + pos += 1 + return s + + def numeric_value(): + nonlocal pos + m = re.match(r"-?\d+", text[pos:]) + if not m: + raise ValueError(f"Ожидалось число на позиции {pos}") + val = m.group(0) + pos += len(val) + return int(val) + + def parse_children(count: int): + nonlocal pos + children = [] + for _ in range(count): + take(",") + child = parse_node() + children.append(child) + return children + + def parse_node() -> Union[Group, Leaf]: + """ + Парсит один объект — либо группу, либо лист + { count, { "Имя", флаг1, флаг2, "Поле4", "Поле5" } } + """ + nonlocal pos + take("{") + count = numeric_value() + take(",") + take("{") + name = parse_value() + take(",") + is_group = numeric_value() + take(",") + menu_flag = numeric_value() + take(",") + replace = parse_value() + take(",") + text_val = parse_value() + take("}") + children = parse_children(count) + take("}") + + # Создаем правильный тип объекта в зависимости от is_group + if int(is_group) == 1: + return Group(count, name, children) + elif int(is_group) == 0: + return Leaf(name, menu_flag, replace, text_val) + else: + raise ValueError(f"Неизвестный значение флага is_group: {is_group}") + + take("{") + count = numeric_value() + root = Root(count, parse_children(count)) + take("}") + assert text[pos:] == "", f"Ожидалось конец файла, но есть остаток: {text[pos:]}" + return root + +if __name__ == "__main__": + if len(sys.argv) < 2: + print("Использование: python parse_skobkofile.py <путь_к_файлу>") + sys.exit(1) + + path = sys.argv[1] + with open(path, "r", encoding="utf-8-sig", errors="ignore") as f: + text = f.read() + + root = parse_skobkofile(text) + print("\n✅ Файл успешно прочитан\n") + root.pretty_print() + + recompiled = root.compile() + + if recompiled == text: + print("✅ Файл успешно скомпилирован и совпадает с исходником") + else: + # запись обратно в контрольный файл + output_path = path + ".out" + with open(output_path, "w", encoding="utf-8-sig") as f: + f.write(recompiled) + + print("❌ Файл успешно скомпилирован, но не совпадает с исходником") + print(f"Скомпилированный файл сохранен в {output_path}") + + source_path = 'temp/src' + root.to_files(source_path) + + root2 = Root.from_files(source_path) + + recompiled = root2.compile() + + if recompiled == text: + print("✅ Файл успешно скомпилирован и совпадает с исходником") + else: + # запись обратно в контрольный файл + output_path = path + ".out" + with open(output_path, "w", encoding="utf-8-sig") as f: + f.write(recompiled) + + print("❌ Файл успешно скомпилирован, но не совпадает с исходником") + print(f"Скомпилированный файл сохранен в {output_path}") diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..f57dc95 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,7 @@ +[build-system] +requires = ["setuptools>=45", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "onec_codetemplate_parser" +version = "0.1.0" \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..4436a4d --- /dev/null +++ b/setup.py @@ -0,0 +1,7 @@ +from setuptools import setup, find_packages + +setup( + name="onec_codetemplate_parser", + version="0.1.0", + packages=find_packages(), +) \ No newline at end of file diff --git a/tests/common.py b/tests/common.py new file mode 100644 index 0000000..e63e405 --- /dev/null +++ b/tests/common.py @@ -0,0 +1,16 @@ +import re + +def check_files_sequential(files: list[str]): + + files.sort() # Сортируем по имени + + expected_number = 1 + for name in files: + m = re.match(r"^(\d{3})\.0_.*", name) + assert m, f"Неверный формат имени папки: {name}" + + number = m.group(1) # первые три цифры + true_number = f'{expected_number:03}' + assert number == true_number, f"Пропущен номер: ожидаем {true_number}, получили {number}" + + expected_number += 1 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..c7ad990 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,30 @@ +import pytest +from onec_codetemplate_parser import main +from pathlib import Path + +TEST_FILE = 'Documents/1C/1c-text-tempates/Надулич.st' + +@pytest.fixture(scope="class") +def test_file_path(): + return Path.home() / TEST_FILE + +@pytest.fixture(scope="class") +def test_data(test_file_path): + file_data = test_file_path.read_text(encoding='utf-8-sig') + return file_data + +@pytest.fixture(scope="class") +def temp_src(tmp_path_factory): + """ + Создаёт временную папку 'src' для теста. + Папка автоматически удаляется после теста. + """ + return tmp_path_factory.mktemp("src") + +@pytest.fixture(scope="class") +def temp_src2(tmp_path_factory): + """ + Создаёт временную папку 'src' для теста. + Папка автоматически удаляется после теста. + """ + return tmp_path_factory.mktemp("src2") diff --git a/tests/test_01.py b/tests/test_01.py new file mode 100644 index 0000000..e4fde75 --- /dev/null +++ b/tests/test_01.py @@ -0,0 +1,45 @@ +from onec_codetemplate_parser import main +from tests.common import check_files_sequential + +class Test_Read_Skobkofile: + + def test_00_test_file_exist(self, test_file_path): + assert test_file_path.exists() + + def test_01_parse_eq_compile(self, test_data): + root = main.parse_skobkofile(test_data) + new_data = root.compile() + assert new_data == test_data + + def test_02_save_and_read(self, test_data, tmp_path): + root = main.parse_skobkofile(test_data) + new_data = root.compile() + + tmp_file = tmp_path / 'tmp.st' + tmp_file.write_text(new_data, encoding='utf-8-sig') + new_data = tmp_file.read_text(encoding='utf-8-sig') + assert new_data == test_data + + +class Test_Write_To_Files: + + def test_00_to_file(self, test_data, temp_src): + root = main.parse_skobkofile(test_data) + root.to_files(temp_src) + + def test_01_to_files(self, temp_src): + # Проверка: есть ли файлы + files = [p for p in temp_src.iterdir() if p.is_file()] + assert len(files) == 0 + + # Проверка: есть ли папки + dirs = [p for p in temp_src.iterdir() if p.is_dir()] + assert len(dirs) == 1 + assert (temp_src / "001.0_Надулич") in dirs + + def test_02_sequential_name(self, temp_src): + d = temp_src / "001.0_Надулич" / "002.0_Комментарии" + subfiles = [p.name for p in d.iterdir() if p.is_dir()] + check_files_sequential(subfiles) + +