1
0
mirror of https://github.com/240596448/onec_codetemplate_parser.git synced 2025-11-23 21:34:39 +02:00
This commit is contained in:
Vladimir Nadulich
2025-10-21 22:33:34 +03:00
commit fd3af97957
9 changed files with 439 additions and 0 deletions

7
.gitignore vendored Normal file
View File

@@ -0,0 +1,7 @@
temp/*
.vscode/*
__pycache__
*.egg-info
.coverage

0
Readme.md Normal file
View File

View File

View File

@@ -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}")

7
pyproject.toml Normal file
View File

@@ -0,0 +1,7 @@
[build-system]
requires = ["setuptools>=45", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "onec_codetemplate_parser"
version = "0.1.0"

7
setup.py Normal file
View File

@@ -0,0 +1,7 @@
from setuptools import setup, find_packages
setup(
name="onec_codetemplate_parser",
version="0.1.0",
packages=find_packages(),
)

16
tests/common.py Normal file
View File

@@ -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

30
tests/conftest.py Normal file
View File

@@ -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")

45
tests/test_01.py Normal file
View File

@@ -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)