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