This commit is contained in:
Egor Deev 2025-09-26 01:40:17 +03:00 committed by GitHub
parent 0c93340da9
commit 6edefc4a8e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 970 additions and 0 deletions

183
bot.py Normal file
View file

@ -0,0 +1,183 @@
import asyncio
import logging
import os
import shutil
import zipfile
from pathlib import Path
from tempfile import TemporaryDirectory
from aiogram import Bot, Dispatcher, F, Router
from aiogram.types import Message, BufferedInputFile
from aiogram.filters import Command
from aiogram.enums.parse_mode import ParseMode
from aiogram.fsm.storage.memory import MemoryStorage
from aiogram.client.bot import DefaultBotProperties
# Импорт наших конвертеров
from md_to_docx import MarkdownToDocxConverter, DocumentSettings
from rep_to_txt import generate_complete_project_structure
# Конфигурация
BOT_TOKEN = "**************************" # @my_convbot
# Инициализация бота
bot = Bot(token=BOT_TOKEN, default=DefaultBotProperties(parse_mode=ParseMode.HTML))
dp = Dispatcher(storage=MemoryStorage())
router = Router()
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
@router.message(Command("start"))
async def start_handler(msg: Message) -> None:
"""Приветственное сообщение"""
await msg.answer(
"<b>Приветствую в своём Конвертере!</b>\n\n"
"📋 <b>Возможности:</b>\n"
"• Отправьте .md файл → получите .docx\n"
"• Отправьте .zip архив → получите структуру проекта в .txt\n\n"
"📝 <b>/help</b> - Для подробной информации"
)
@router.message(Command("help"))
async def help_handler(msg: Message) -> None:
"""Подробная справка"""
help_text = (
"<b>📚 Подробное руководство</b>\n\n"
"<b>1. Конвертация Markdown → DOCX:</b>\n"
"• Отправьте .md файл\n"
"• Получите DOCX с форматированием по ГОСТ\n\n"
"<b>2. Анализ архива → TXT:</b>\n"
"• Отправьте .zip архив\n"
"• Получите полную структуру проекта в текстовом файле\n\n"
"<b>⚡ Ограничения:</b>\n"
"• Размер файла: до 20 МБ\n"
"• Поддерживаемые форматы: .md, .zip"
)
await msg.answer(help_text)
@router.message(F.document)
async def handle_document(msg: Message) -> None:
"""Обработка загруженных документов"""
document = msg.document
file_name = document.file_name
file_size = document.file_size
if file_size > 20 * 1024 * 1024:
await msg.answer("❌ Файл слишком большой! Максимум 20 МБ")
return
file_ext = Path(file_name).suffix.lower()
status_msg = await msg.answer("⏳ Обрабатываю файл...")
try:
with TemporaryDirectory() as temp_dir:
# Скачиваем файл
file_info = await bot.get_file(document.file_id)
input_path = os.path.join(temp_dir, file_name)
await bot.download_file(file_info.file_path, input_path)
if file_ext == '.md':
# Конвертация MD → DOCX
output_path = await convert_md_to_docx(input_path, temp_dir)
output_name = Path(file_name).stem + '.docx'
elif file_ext in ['.zip']:
# Анализ архива → TXT
output_path = await analyze_archive(input_path, temp_dir, file_ext)
output_name = Path(file_name).stem + '_structure.txt'
else:
await status_msg.edit_text("❌ Неподдерживаемый формат файла!")
return
# Отправка результата
with open(output_path, 'rb') as output_file:
result_file = BufferedInputFile(
output_file.read(),
filename=output_name
)
await msg.answer_document(result_file)
await status_msg.edit_text("✅ Конвертация завершена!")
except Exception as e:
logger.error(f"Ошибка обработки файла: {e}")
await status_msg.edit_text(f"❌ Ошибка обработки: {str(e)}")
async def convert_md_to_docx(md_path: str, temp_dir: str) -> str:
"""Конвертация Markdown в DOCX"""
output_path = os.path.join(temp_dir, "output.docx")
# Настройки
settings = DocumentSettings()
settings.font_name = "Times New Roman"
settings.font_size = 14
settings.line_spacing = 1.5
settings.margin_left = 3.0
settings.auto_numbering_headings = True
converter = MarkdownToDocxConverter(settings)
converter.convert(md_path, output_path)
return output_path
async def analyze_archive(archive_path: str, temp_dir: str, file_ext: str) -> str:
"""Анализ архива и создание структуры проекта"""
extract_dir = os.path.join(temp_dir, "extracted")
os.makedirs(extract_dir, exist_ok=True)
# Извлечение архива
with zipfile.ZipFile(archive_path, 'r') as zip_ref:
zip_ref.extractall(extract_dir)
# Поиск основной папки проекта
extracted_items = os.listdir(extract_dir)
if len(extracted_items) == 1 and os.path.isdir(os.path.join(extract_dir, extracted_items[0])):
project_root = os.path.join(extract_dir, extracted_items[0])
else:
project_root = extract_dir
# Генерация структуры
structure = generate_complete_project_structure(project_root)
# Сохранение в файл
output_path = os.path.join(temp_dir, "project_structure.txt")
with open(output_path, 'w', encoding='utf-8') as f:
f.write(structure)
return output_path
@router.message()
async def handle_other_messages(msg: Message) -> None:
"""Обработка остальных сообщений"""
await msg.answer(
"<b>Отправьте файл для конвертации:</b>\n"
"• .md файл для конвертации в DOCX\n"
"• .zip архив для анализа структуры\n\n"
"Используйте /help для подробной справки!"
)
async def main() -> None:
"""Запуск бота"""
try:
logger.info("Запуск File Converter Bot...")
dp.include_router(router)
await bot.delete_webhook(drop_pending_updates=True)
logger.info("Bot started successfully")
await dp.start_polling(bot)
except Exception as e:
logger.critical(f"Критическая ошибка: {e}")
finally:
await bot.session.close()
if __name__ == "__main__":
try:
asyncio.run(main())
except KeyboardInterrupt:
logger.info("Bot stopped by user")
except Exception as e:
logger.critical(f"Unhandled exception: {e}")

622
md_to_docx.py Normal file
View file

@ -0,0 +1,622 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import re
import sys
from pathlib import Path
from docx import Document
from docx.shared import Inches, Pt, RGBColor, Cm
from docx.enum.text import WD_ALIGN_PARAGRAPH, WD_LINE_SPACING
from docx.enum.style import WD_STYLE_TYPE
from docx.enum.section import WD_SECTION
from docx.oxml.shared import OxmlElement, qn
from docx.oxml.ns import nsdecls
from docx.oxml import parse_xml
class DocumentSettings:
"""Настройки форматирования документа с поддержкой ГОСТ"""
def __init__(self):
# Базовые настройки текста
self.font_name = "Times New Roman"
self.font_size = 14 # основной текст
self.line_spacing = 1.5
self.justify_text = True
self.paragraph_spacing = 6
self.text_color = (0, 0, 0)
self.paragraph_indent = 1.25
# Отступы от полей документа в сантиметрах (ГОСТ 7.32-2017)
self.margin_top = 2.0
self.margin_bottom = 2.0
self.margin_left = 3.0 # увеличено для переплета
self.margin_right = 1.5
# Настройки шрифтов заголовков по ГОСТ
self.heading1_font_size = 16 # Заголовки глав
self.heading2_font_size = 14 # Заголовки разделов
self.heading3_font_size = 14 # Подзаголовки
self.heading4_font_size = 14
self.heading5_font_size = 12
self.heading6_font_size = 12
self.footnote_font_size = 10
# Интервалы заголовков по ГОСТ
self.heading_spacing_before = 12 # пт
self.heading_spacing_after = 6 # пт
self.paragraph_spacing_before = 0 # пт
# Нумерация страниц
self.page_numbering = True
self.page_number_position = "bottom_center" # top_right, bottom_center, bottom_right
self.page_number_start = 1
self.exclude_title_page_numbering = True
# Автонумерация заголовков
self.auto_numbering_headings = False
self.numbering_format = "decimal" # "decimal" (1.1.1) или "simple" (1)
# Дополнительные ГОСТ настройки
self.bibliography_style = "gost"
self.table_caption_position = "above" # above, below
self.figure_caption_position = "below"
class MarkdownToDocxConverter:
"""Конвертер Markdown в DOCX с поддержкой ГОСТ"""
def __init__(self, settings: DocumentSettings = None):
self.settings = settings or DocumentSettings()
self.doc = Document()
# Счетчики для автонумерации
self.heading_counters = [0] * 6 # для 6 уровней заголовков
self.footnote_counter = 0
self.table_counter = 0
self.figure_counter = 0
self.setup_document_margins()
self.setup_page_numbering()
self.setup_styles()
def setup_document_margins(self):
"""Настройка отступов от полей документа по ГОСТ"""
sections = self.doc.sections
for section in sections:
section.top_margin = Cm(self.settings.margin_top)
section.bottom_margin = Cm(self.settings.margin_bottom)
section.left_margin = Cm(self.settings.margin_left)
section.right_margin = Cm(self.settings.margin_right)
def setup_page_numbering(self):
"""Настройка нумерации страниц согласно ГОСТ"""
if not self.settings.page_numbering:
return
section = self.doc.sections[0]
# Создание колонтитула для нумерации
if self.settings.page_number_position == "bottom_center":
footer = section.footer
footer_para = footer.paragraphs[0]
footer_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
elif self.settings.page_number_position == "top_right":
header = section.header
header_para = header.paragraphs[0]
header_para.alignment = WD_ALIGN_PARAGRAPH.RIGHT
elif self.settings.page_number_position == "bottom_right":
footer = section.footer
footer_para = footer.paragraphs[0]
footer_para.alignment = WD_ALIGN_PARAGRAPH.RIGHT
def setup_styles(self):
"""Настройка стилей документа в соответствии с ГОСТ"""
styles = self.doc.styles
# Настройка базового стиля
normal_style = styles['Normal']
normal_font = normal_style.font
normal_font.name = self.settings.font_name
normal_font.size = Pt(self.settings.font_size)
normal_font.color.rgb = RGBColor(*self.settings.text_color)
normal_paragraph = normal_style.paragraph_format
normal_paragraph.line_spacing_rule = WD_LINE_SPACING.MULTIPLE
normal_paragraph.line_spacing = self.settings.line_spacing
normal_paragraph.space_after = Pt(self.settings.paragraph_spacing)
normal_paragraph.space_before = Pt(self.settings.paragraph_spacing_before)
normal_paragraph.first_line_indent = Cm(self.settings.paragraph_indent)
if self.settings.justify_text:
normal_paragraph.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
# Настройка стилей заголовков с дифференцированными размерами
heading_sizes = [
self.settings.heading1_font_size,
self.settings.heading2_font_size,
self.settings.heading3_font_size,
self.settings.heading4_font_size,
self.settings.heading5_font_size,
self.settings.heading6_font_size
]
for i in range(1, 7):
heading_style_name = f'Heading {i}'
if heading_style_name in [s.name for s in styles]:
heading_style = styles[heading_style_name]
else:
heading_style = styles.add_style(heading_style_name, WD_STYLE_TYPE.PARAGRAPH)
heading_font = heading_style.font
heading_font.name = self.settings.font_name
heading_font.size = Pt(heading_sizes[i-1]) # используем соответствующий размер
heading_font.bold = True
heading_font.color.rgb = RGBColor(*self.settings.text_color)
heading_paragraph = heading_style.paragraph_format
heading_paragraph.space_before = Pt(self.settings.heading_spacing_before)
heading_paragraph.space_after = Pt(self.settings.heading_spacing_after)
# Заголовки 1 и 2 уровня по центру (ГОСТ), остальные с отступом
if i <= 2:
heading_paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER
heading_paragraph.first_line_indent = Cm(0)
else:
if self.settings.justify_text:
heading_paragraph.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
heading_paragraph.first_line_indent = Cm(self.settings.paragraph_indent)
# Стиль для сносок
try:
footnote_style = styles.add_style('Footnote', WD_STYLE_TYPE.PARAGRAPH)
footnote_font = footnote_style.font
footnote_font.name = self.settings.font_name
footnote_font.size = Pt(self.settings.footnote_font_size)
footnote_font.color.rgb = RGBColor(*self.settings.text_color)
footnote_paragraph = footnote_style.paragraph_format
footnote_paragraph.space_before = Pt(3)
footnote_paragraph.space_after = Pt(3)
footnote_paragraph.first_line_indent = Cm(0.5)
except:
pass
# Стиль для кода (без изменений)
try:
code_style = styles.add_style('Code', WD_STYLE_TYPE.CHARACTER)
code_font = code_style.font
code_font.name = 'Courier New'
code_font.size = Pt(self.settings.font_size)
code_font.color.rgb = RGBColor(*self.settings.text_color)
except:
pass
# Стиль для блоков кода
try:
code_block_style = styles.add_style('Code Block', WD_STYLE_TYPE.PARAGRAPH)
code_block_font = code_block_style.font
code_block_font.name = 'Courier New'
code_block_font.size = Pt(self.settings.font_size)
code_block_font.color.rgb = RGBColor(*self.settings.text_color)
code_block_paragraph = code_block_style.paragraph_format
code_block_paragraph.left_indent = Inches(0.5)
code_block_paragraph.first_line_indent = Cm(0) # без отступа первой строки для кода
code_block_paragraph.space_before = Pt(6)
code_block_paragraph.space_after = Pt(6)
except:
pass
# Стиль для подписей к таблицам и рисункам
try:
caption_style = styles.add_style('Caption', WD_STYLE_TYPE.PARAGRAPH)
caption_font = caption_style.font
caption_font.name = self.settings.font_name
caption_font.size = Pt(self.settings.font_size - 2) # меньше основного текста
caption_font.color.rgb = RGBColor(*self.settings.text_color)
caption_paragraph = caption_style.paragraph_format
caption_paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER
caption_paragraph.space_before = Pt(6)
caption_paragraph.space_after = Pt(6)
except:
pass
def generate_heading_number(self, level: int) -> str:
"""Генерация номера заголовка согласно настройкам автонумерации"""
if not self.settings.auto_numbering_headings:
return ""
# Обновляем счетчик текущего уровня
self.heading_counters[level - 1] += 1
# Обнуляем счетчики всех нижестоящих уровней
for i in range(level, 6):
self.heading_counters[i] = 0
if self.settings.numbering_format == "simple":
return f"{self.heading_counters[level - 1]}. "
else: # decimal
# Формируем иерархическую нумерацию
numbers = []
for i in range(level):
if self.heading_counters[i] > 0:
numbers.append(str(self.heading_counters[i]))
return ".".join(numbers) + ". " if numbers else ""
def parse_markdown_file(self, file_path: str):
"""Чтение и парсинг Markdown файла"""
try:
with open(file_path, 'r', encoding='utf-8') as file:
content = file.read()
return content
except Exception as e:
raise Exception(f"Ошибка чтения файла: {e}")
def add_text_run_with_color(self, paragraph, text, bold=False, italic=False, code_style=False):
"""Добавление текста с настройкой цвета"""
run = paragraph.add_run(text)
run.font.color.rgb = RGBColor(*self.settings.text_color)
if bold:
run.font.bold = True
if italic:
run.font.italic = True
if code_style:
run.style = 'Code'
return run
def process_text_formatting(self, text: str, paragraph):
"""Обработка форматирования текста включая сноски [^1]"""
# Обработка сносок
footnote_pattern = r'\[\^(\d+)\]'
footnotes = re.findall(footnote_pattern, text)
# Заменяем сноски на верхние индексы
for footnote_num in footnotes:
text = re.sub(rf'\[\^{footnote_num}\]', f'{{FOOTNOTE_{footnote_num}}}', text)
# Разбор текста на части с различным форматированием
parts = re.split(r'(\*\*.*?\*\*|\*.*?\*|`.*?`|\{FOOTNOTE_\d+\})', text)
for part in parts:
if not part:
continue
if part.startswith('**') and part.endswith('**'):
# Жирный текст
self.add_text_run_with_color(paragraph, part[2:-2], bold=True)
elif part.startswith('*') and part.endswith('*'):
# Курсив
self.add_text_run_with_color(paragraph, part[1:-1], italic=True)
elif part.startswith('`') and part.endswith('`'):
# Инлайн код
self.add_text_run_with_color(paragraph, part[1:-1], code_style=True)
elif part.startswith('{FOOTNOTE_') and part.endswith('}'):
# Сноска - добавляем как верхний индекс
footnote_num = re.search(r'FOOTNOTE_(\d+)', part).group(1)
run = self.add_text_run_with_color(paragraph, footnote_num)
run.font.superscript = True
else:
# Обычный текст
self.add_text_run_with_color(paragraph, part)
def process_list(self, lines: list, start_idx: int):
"""Обработка списков с правильным форматированием по ГОСТ"""
i = start_idx
list_items = []
while i < len(lines):
line = lines[i].strip()
if re.match(r'^[-*+]\s', line):
item_text = re.sub(r'^[-*+]\s', '', line)
list_items.append(('bullet', item_text, 0))
elif re.match(r'^\d+\.\s', line):
item_text = re.sub(r'^\d+\.\s', '', line)
list_items.append(('number', item_text, 0))
elif re.match(r'^ [-*+]\s', line):
item_text = re.sub(r'^ [-*+]\s', '', line)
list_items.append(('bullet', item_text, 1))
elif re.match(r'^ \d+\.\s', line):
item_text = re.sub(r'^ \d+\.\s', '', line)
list_items.append(('number', item_text, 1))
elif line == '':
i += 1
continue
else:
break
i += 1
# Добавление элементов списка с настройками ГОСТ
for list_type, text, level in list_items:
paragraph = self.doc.add_paragraph()
paragraph.paragraph_format.left_indent = Cm(level * 0.75) # увеличенный отступ для вложенности
paragraph.paragraph_format.first_line_indent = Cm(self.settings.paragraph_indent)
if list_type == 'bullet':
paragraph.style = 'List Bullet'
# Используем тире вместо точек (согласно ГОСТ)
bullet_run = paragraph.runs[0] if paragraph.runs else paragraph.add_run()
bullet_run.text = " " # длинное тире
else:
paragraph.style = 'List Number'
self.process_text_formatting(text, paragraph)
return i - 1
def process_table(self, lines: list, start_idx: int):
"""Обработка таблиц с подписями согласно ГОСТ"""
i = start_idx
table_lines = []
while i < len(lines):
line = lines[i].strip()
if '|' in line:
table_lines.append(line)
elif line == '':
i += 1
continue
else:
break
i += 1
if len(table_lines) < 2:
return start_idx
# Добавляем подпись к таблице (если настроено)
if self.settings.table_caption_position == "above":
self.table_counter += 1
caption_para = self.doc.add_paragraph()
caption_para.style = 'Caption'
caption_para.add_run(f"Таблица {self.table_counter}")
# Парсинг и создание таблицы
headers = [cell.strip() for cell in table_lines[0].split('|')[1:-1]]
data_lines = table_lines[2:] if len(table_lines) > 2 else []
table = self.doc.add_table(rows=1, cols=len(headers))
table.style = 'Table Grid'
# Заполнение заголовков
header_row = table.rows[0]
for idx, header in enumerate(headers):
cell = header_row.cells[idx]
cell.text = header
for paragraph in cell.paragraphs:
paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER
for run in paragraph.runs:
run.font.bold = True
# Заполнение данных
for line in data_lines:
row_data = [cell.strip() for cell in line.split('|')[1:-1]]
row = table.add_row()
for idx, cell_data in enumerate(row_data):
if idx < len(row.cells):
row.cells[idx].text = cell_data
# Выравнивание по центру для всех ячеек (ГОСТ)
for paragraph in row.cells[idx].paragraphs:
paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER
# Подпись снизу (если настроено)
if self.settings.table_caption_position == "below":
self.table_counter += 1
caption_para = self.doc.add_paragraph()
caption_para.style = 'Caption'
caption_para.add_run(f"Таблица {self.table_counter}")
return i - 1
def process_code_block(self, lines: list, start_idx: int):
"""Обработка блоков кода"""
i = start_idx + 1
code_lines = []
while i < len(lines):
line = lines[i]
if line.strip().startswith('```'):
break
code_lines.append(line)
i += 1
code_paragraph = self.doc.add_paragraph()
code_paragraph.style = 'Code Block'
code_paragraph.add_run('\n'.join(code_lines))
return i
def add_footnote_definition(self, footnote_num: str, footnote_text: str):
"""Добавление определения сноски в конец документа"""
footnote_para = self.doc.add_paragraph()
footnote_para.style = 'Footnote'
# Номер сноски как верхний индекс
footnote_run = footnote_para.add_run(footnote_num)
footnote_run.font.superscript = True
# Текст сноски
footnote_para.add_run(f" {footnote_text}")
def process_bibliography(self, lines: list, start_idx: int):
"""Обработка списка литературы в стиле ГОСТ"""
i = start_idx
bib_items = []
# Поиск элементов библиографии
while i < len(lines):
line = lines[i].strip()
if re.match(r'^\d+\.\s', line):
bib_text = re.sub(r'^\d+\.\s', '', line)
bib_items.append(bib_text)
elif line == '':
i += 1
continue
else:
break
i += 1
if bib_items:
# Заголовок списка литературы
bib_heading = self.doc.add_paragraph()
bib_heading.style = 'Heading 1'
bib_heading.add_run("СПИСОК ЛИТЕРАТУРЫ")
# Элементы библиографии
for idx, item in enumerate(bib_items, 1):
bib_para = self.doc.add_paragraph()
bib_para.paragraph_format.first_line_indent = Cm(0)
bib_para.paragraph_format.left_indent = Cm(1)
bib_para.add_run(f"{idx}. {item}")
return i - 1
def convert(self, md_file_path: str, output_path: str = None):
"""Основной метод конвертации с поддержкой ГОСТ"""
if not output_path:
md_path = Path(md_file_path)
output_path = md_path.with_suffix('.docx')
content = self.parse_markdown_file(md_file_path)
lines = content.split('\n')
# Сбор сносок для обработки в конце
footnote_definitions = {}
i = 0
while i < len(lines):
line = lines[i]
stripped_line = line.strip()
if not stripped_line:
i += 1
continue
# Обработка определений сносок [^1]: текст сноски
footnote_def_match = re.match(r'^\[\^(\d+)\]:\s*(.+)', stripped_line)
if footnote_def_match:
footnote_num = footnote_def_match.group(1)
footnote_text = footnote_def_match.group(2)
footnote_definitions[footnote_num] = footnote_text
i += 1
continue
# Заголовки с автонумерацией
if stripped_line.startswith('#'):
match = re.match(r'^(#{1,6})\s+(.+)', stripped_line)
if match:
level = len(match.group(1))
title = match.group(2)
# Разрыв страницы перед заголовком 2 уровня
if level == 2:
self.doc.add_page_break()
heading = self.doc.add_paragraph()
heading.style = f'Heading {level}'
# Добавляем автонумерацию
heading_number = self.generate_heading_number(level)
full_title = heading_number + title
self.process_text_formatting(full_title, heading)
# Блоки кода
elif stripped_line.startswith('```'):
i = self.process_code_block(lines, i)
# Таблицы
elif '|' in stripped_line:
i = self.process_table(lines, i)
# Списки
elif re.match(r'^[-*+]\s', stripped_line) or re.match(r'^\d+\.\s', stripped_line):
i = self.process_list(lines, i)
# Список литературы (если заголовок содержит "литература" или "bibliography")
elif re.match(r'^#+\s*(список\s+литературы|bibliography|references)', stripped_line, re.IGNORECASE):
i = self.process_bibliography(lines, i + 1)
# Цитаты
elif stripped_line.startswith('>'):
quote_text = re.sub(r'^>\s?', '', stripped_line)
quote_paragraph = self.doc.add_paragraph()
quote_paragraph.paragraph_format.left_indent = Inches(0.5)
quote_paragraph.paragraph_format.right_indent = Inches(0.5)
self.process_text_formatting(quote_text, quote_paragraph)
for run in quote_paragraph.runs:
run.font.italic = True
# Горизонтальные линии
elif stripped_line in ['---', '***', '___']:
hr_paragraph = self.doc.add_paragraph()
hr_paragraph.add_run('_' * 50)
hr_paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER
# Обычные абзацы
else:
paragraph = self.doc.add_paragraph()
self.process_text_formatting(stripped_line, paragraph)
i += 1
# Добавление сносок в конец документа
if footnote_definitions:
# Разделительная линия
self.doc.add_paragraph().add_run('_' * 50)
for footnote_num in sorted(footnote_definitions.keys(), key=int):
self.add_footnote_definition(footnote_num, footnote_definitions[footnote_num])
self.doc.save(output_path)
return output_path
def main():
"""Основная функция для запуска из командной строки"""
if len(sys.argv) < 2:
print("Использование: python md_converter.py <путь_к_md_файлу> [путь_к_выходномуайлу]")
return
md_file = sys.argv[1]
output_file = sys.argv[2] if len(sys.argv) > 2 else None
# ГОСТ-совместимые настройки по умолчанию
settings = DocumentSettings()
converter = MarkdownToDocxConverter(settings)
try:
output_path = converter.convert(md_file, output_file)
print(f"Файл успешно конвертирован: {output_path}")
except Exception as e:
print(f"Ошибка конвертации: {e}")
if __name__ == "__main__":
main()
# Пример использования с кастомными ГОСТ настройками:
"""
settings = DocumentSettings()
settings.font_name = "Times New Roman"
settings.font_size = 14
settings.heading1_font_size = 16
settings.heading2_font_size = 14
settings.line_spacing = 1.5
settings.margin_left = 3.0 # для переплета
settings.auto_numbering_headings = True
settings.numbering_format = "decimal" # 1.1.1 формат
settings.page_numbering = True
settings.page_number_position = "bottom_center"
converter = MarkdownToDocxConverter(settings)
converter.convert("dissertation.md", "dissertation_gost.docx")
"""

165
rep_to_txt.py Normal file
View file

@ -0,0 +1,165 @@
import os
IGNORE_PATTERNS = {
'.git', '.svn', '.hg', # Version control systems
'__pycache__', '.pytest_cache', # Python artifacts
'node_modules', '.npm', # Node.js dependencies
'target', 'build', 'dist', # Build outputs
'.idea', '.vscode', # IDE metadata
'.DS_Store', 'Thumbs.db', # OS metadata
'.pro.user' # QT user config
}
BINARY_EXTENSIONS = {
'.exe', '.dll', '.so', '.dylib', '.zip', '.tar', '.gz', '.rar', '.7z',
'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.ico', '.svg', '.webp',
'.mp3', '.mp4', '.avi', '.mov', '.mkv', '.wmv', '.flv',
'.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx',
'.bin', '.dat', '.db', '.sqlite', '.mdb'
}
def scan_directory(path, prefix=""):
"""Рекурсивное сканирование с форматированием дерева"""
items = []
try:
entries = sorted(os.listdir(path))
# Critical filtering layer для performance optimization
dirs = [e for e in entries if os.path.isdir(os.path.join(path, e)) and e not in IGNORE_PATTERNS]
files = [e for e in entries if os.path.isfile(os.path.join(path, e)) and e not in IGNORE_PATTERNS]
all_items = dirs + files
for i, item in enumerate(all_items):
item_path = os.path.join(path, item)
is_last_item = (i == len(all_items) - 1)
if is_last_item:
current_prefix = prefix + "└── "
next_prefix = prefix + " "
else:
current_prefix = prefix + "├── "
next_prefix = prefix + ""
items.append(current_prefix + item)
if os.path.isdir(item_path):
items.extend(scan_directory(item_path, next_prefix))
except PermissionError:
items.append(prefix + "└── [Access Denied]")
return items
def generate_complete_project_structure(root_path):
"""Генератор проектной документации корпоративного уровня"""
if not os.path.exists(root_path):
return f"Error: Path {root_path} does not exist"
result = []
# Этап 1: Создание древовидной структуры
root_name = os.path.basename(root_path) or root_path
result.append(root_name)
result.extend(scan_directory(root_path))
# Этап 2: Полное извлечение содержимого файла
result.append("\n") # Separator между разделами дерева и содержимым
result.extend(extract_all_file_contents(root_path))
return "\n".join(result)
def extract_all_file_contents(root_path):
"""Механизм извлечения контента с обработкой файлов"""
content_lines = []
for root, dirs, files in os.walk(root_path):
dirs[:] = [d for d in dirs if d not in IGNORE_PATTERNS]
for file in sorted(files):
if file in IGNORE_PATTERNS:
continue
file_path = os.path.join(root, file)
relative_path = os.path.relpath(file_path, root_path)
content_lines.extend(process_single_file(relative_path, file_path))
return content_lines
def process_single_file(relative_path, file_path):
"""Обработка файлов"""
content_lines = []
# Раздел заголовка
content_lines.append("\n" + "-" * 80)
content_lines.append(f"{relative_path}:")
content_lines.append("-" * 80)
file_ext = os.path.splitext(relative_path)[1].lower()
# Обнаружение двоичных файлов и генерация URL-адресов
if file_ext in BINARY_EXTENSIONS or is_likely_binary(file_path):
# GitHub raw URL
if file_ext in {'.png', '.jpg', '.jpeg', '.gif', '.svg', '.ico'}:
# Структура URL - настраивается на основе фактического хранилища
github_url = f"https://raw.githubusercontent.com/.../{relative_path.replace(os.sep, '/')}"
content_lines.append(github_url)
else:
content_lines.append("[Binary file - content not displayed]")
else:
# Извлечение содержимого текстового файла
content_lines.extend(extract_text_content(file_path))
content_lines.append("")
return content_lines
def extract_text_content(file_path):
"""Резервное извлечение с несколькими кодировками"""
encodings_priority = ['utf-8', 'utf-8-sig', 'cp1251', 'latin1', 'cp1252']
for encoding in encodings_priority:
try:
with open(file_path, 'r', encoding=encoding) as f:
lines = f.readlines()
return [f"{i:4} | {line.rstrip()}" for i, line in enumerate(lines, 1)]
except (UnicodeDecodeError, UnicodeError):
continue
except Exception as e:
return [f"ERROR: Не удается прочитать файл - {e}"]
return ["WARNING: Кодировка файла, не поддерживаемая для извлечения текста"]
def is_likely_binary(file_path):
"""Эвристическое обнаружение двоичных файлов для крайних случаев"""
try:
with open(file_path, 'rb') as f:
chunk = f.read(8192)
# Обнаружение нулевого байта - надежный бинарный индикатор
return b'\x00' in chunk
except:
return True
if __name__ == "__main__":
# Конфигурация: измените путь к целевому каталогу проекта
project_path = r"D:\Programs\GitHub\deev.space\static"
# project_path = r"D:/Programs/GitHub/openoffice"
# project_path = "."
print("Приступаем к формированию комплексной структуры проекта...")
tree_output = generate_complete_project_structure(project_path)
output_filename = project_path.split('\\')[-1] + "_rep.txt"
try:
with open(output_filename, "w", encoding="utf-8") as f:
f.write(tree_output)
print(f"\nПолная проектная документация, сохраненная в: {output_filename}")
except Exception as e:
print(f"Предупреждение: Не удалось сохранить файл - {e}")
print("Формирование структуры проекта успешно завершено!")