You've already forked onec_codetemplate_parser
mirror of
https://github.com/240596448/onec_codetemplate_parser.git
synced 2025-11-23 21:34:39 +02:00
Init
This commit is contained in:
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
temp/*
|
||||
.vscode/*
|
||||
|
||||
__pycache__
|
||||
*.egg-info
|
||||
|
||||
.coverage
|
||||
0
onec_codetemplate_parser/__init__.py
Normal file
0
onec_codetemplate_parser/__init__.py
Normal file
327
onec_codetemplate_parser/main.py
Normal file
327
onec_codetemplate_parser/main.py
Normal 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
7
pyproject.toml
Normal 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
7
setup.py
Normal 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
16
tests/common.py
Normal 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
30
tests/conftest.py
Normal 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
45
tests/test_01.py
Normal 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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user