diff --git a/.gitignore b/.gitignore index 1be18a2..d714ca7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,48 @@ -.idea -.venv -*.docx \ No newline at end of file +``` +# Compiled and build artifacts +*.pyc +__pycache__/ +*.o +*.obj + +# Dependencies +venv/ +.venv/ +node_modules/ +*.egg-info/ +dist/ +build/ +*.so +*.dylib +*.dll + +# Logs and temp files +*.log +*.tmp +*.swp +*.swo + +# Environment +.env +.env.local +.env.* + +# Editors +.vscode/ +.idea/ +*.swp +*.swo + +# Coverage +.coverage +coverage/ +htmlcov/ + +# OS +.DS_Store +Thumbs.db + +# Testing +.pytest_cache/ +.mypy_cache/ +``` \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..8ae3ba3 --- /dev/null +++ b/README.md @@ -0,0 +1,93 @@ +# GhostEditor - Markdown to Word Converter + +A web-based application that converts Markdown to Word documents with advanced features including real-time preview, styling options, auto-captioning, and table pagination. + +## Features + +1. **Real-time Conversion**: See your Markdown text convert to Word format in real-time +2. **Styling Options**: Apply different styles to text elements (headings, quotes, code blocks, etc.) +3. **Auto Captioning**: Automatic captions for images, tables, and figures +4. **Table Pagination**: Support for tables that span across pages with repeating headers +5. **Custom Settings**: Configure document settings and styling preferences + +## Requirements + +- Python 3.7+ +- Flask +- python-docx +- markdown + +## Installation + +1. Install the required packages: + ```bash + pip install -r requirements.txt + ``` + +2. Run the application: + ```bash + python app.py + ``` + +3. Open your browser and navigate to `http://localhost:5000` + +## Usage + +1. Enter or paste your Markdown content in the left editor panel +2. See the real-time conversion in the right preview panel +3. Use the settings panel to configure document options: + - Set document title + - Choose default text style + - Enable/disable auto-captioning for images, tables, and figures + - Enable/disable table pagination +4. Use the toolbar buttons to format text directly in the editor +5. Select block type and apply custom styles to specific elements +6. Add custom captions to elements as needed +7. Click "Export to Word" to download your document + +## Features Explained + +### Real-time Preview +- The application converts Markdown to Word format as you type +- Preview updates automatically as you make changes + +### Auto Captioning +- Images automatically receive "Figure X" captions +- Tables automatically receive "Table X" captions +- Captions are numbered sequentially + +### Table Pagination +- When enabled, tables that span multiple pages will repeat their header row +- This ensures readability when tables cross page boundaries + +### Block Styling +- Select different block types (paragraph, heading, list, etc.) +- Apply specific styles to different content blocks +- Add custom captions to any element + +## File Structure + +``` +/workspace/ +├── app.py # Main Flask application +├── requirements.txt # Python dependencies +├── templates/ +│ └── index.html # Main HTML template +└── static/ + ├── style.css # CSS styling + └── script.js # JavaScript functionality +``` + +## Technologies Used + +- **Frontend**: HTML, CSS, JavaScript, Bootstrap 5, Marked.js +- **Backend**: Flask (Python) +- **Document Processing**: python-docx +- **Markdown Processing**: markdown (Python library) + +## Customization + +You can customize the application by modifying: +- `static/style.css` for visual styling +- `static/script.js` for client-side functionality +- `app.py` for server-side processing and document generation \ No newline at end of file diff --git a/ReadMe.md b/ReadMe.md index 900fa43..8ae3ba3 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -1,209 +1,93 @@ -# GhostEditor -Программа для конвертации из формата `md` в формат `docx`(x это без макросов, m с макросами) (`Office Open XML`),`odt`(`OpenDocument`) +# GhostEditor - Markdown to Word Converter -#### **1. python-docx** -**Назначение**: Создание и модификация Word-документов. -**Ключевые возможности**: -- Добавление/редактирование текста, таблиц, изображений. -- Управление стилями, колонтитулами, сносками. -- Работа с XML через абстракции (Paragraph, Run, Table). +A web-based application that converts Markdown to Word documents with advanced features including real-time preview, styling options, auto-captioning, and table pagination. -**Пример**: -```python -from docx import Document -from docx.shared import Pt +## Features -doc = Document() -paragraph = doc.add_paragraph("Hello ") -run = paragraph.add_run("OOXML!") -run.bold = True -run.font.size = Pt(14) +1. **Real-time Conversion**: See your Markdown text convert to Word format in real-time +2. **Styling Options**: Apply different styles to text elements (headings, quotes, code blocks, etc.) +3. **Auto Captioning**: Automatic captions for images, tables, and figures +4. **Table Pagination**: Support for tables that span across pages with repeating headers +5. **Custom Settings**: Configure document settings and styling preferences -# Таблица -table = doc.add_table(rows=2, cols=2) -table.cell(0, 0).text = "Cell A1" +## Requirements -doc.save("report.docx") +- Python 3.7+ +- Flask +- python-docx +- markdown + +## Installation + +1. Install the required packages: + ```bash + pip install -r requirements.txt + ``` + +2. Run the application: + ```bash + python app.py + ``` + +3. Open your browser and navigate to `http://localhost:5000` + +## Usage + +1. Enter or paste your Markdown content in the left editor panel +2. See the real-time conversion in the right preview panel +3. Use the settings panel to configure document options: + - Set document title + - Choose default text style + - Enable/disable auto-captioning for images, tables, and figures + - Enable/disable table pagination +4. Use the toolbar buttons to format text directly in the editor +5. Select block type and apply custom styles to specific elements +6. Add custom captions to elements as needed +7. Click "Export to Word" to download your document + +## Features Explained + +### Real-time Preview +- The application converts Markdown to Word format as you type +- Preview updates automatically as you make changes + +### Auto Captioning +- Images automatically receive "Figure X" captions +- Tables automatically receive "Table X" captions +- Captions are numbered sequentially + +### Table Pagination +- When enabled, tables that span multiple pages will repeat their header row +- This ensures readability when tables cross page boundaries + +### Block Styling +- Select different block types (paragraph, heading, list, etc.) +- Apply specific styles to different content blocks +- Add custom captions to any element + +## File Structure + +``` +/workspace/ +├── app.py # Main Flask application +├── requirements.txt # Python dependencies +├── templates/ +│ └── index.html # Main HTML template +└── static/ + ├── style.css # CSS styling + └── script.js # JavaScript functionality ``` -**Плюсы**: -- Интуитивный API, близкий к логике Word. -- Поддержка всех ключевых частей пакета (headers, footers, styles). +## Technologies Used -**Минусы**: -- Нет поддержки макросов. -- Ограниченная работа с графикой (изображения добавляются, но редактирование сложно). +- **Frontend**: HTML, CSS, JavaScript, Bootstrap 5, Marked.js +- **Backend**: Flask (Python) +- **Document Processing**: python-docx +- **Markdown Processing**: markdown (Python library) -**Ссылка**: [python-docx.readthedocs.io](https://python-docx.readthedocs.io/) +## Customization ---- - -#### **2. docxtpl** -**Назначение**: Генерация документов по шаблонам (Jinja2). -**Особенности**: -- Шаблоны .docx с переменными (`{{ variable }}`) и логикой (`{% if %}`). -- Поддержка таблиц, изображений, HTML-фрагментов. - -**Пример**: -```python -from docxtpl import DocxTemplate - -doc = DocxTemplate("template.docx") -context = { - "title": "Отчет за 2023", - "items": [{"name": "Item 1"}, {"name": "Item 2"}] -} -doc.render(context) -doc.save("output.docx") -``` - -**Плюсы**: -- Идеален для шаблонизированных отчетов (договоры, счета). -- Интеграция с Django/Flask. - -**Минусы**: -- Требует предварительной настройки шаблонов в Word. - -**Ссылка**: [github.com/elapouya/python-docx-template](https://github.com/elapouya/python-docx-template) - ---- - -#### **3. mammoth** -**Назначение**: Конвертация .docx → HTML с сохранением структуры. -**Особенности**: -- Преобразует стили Word в CSS-классы. -- Подходит для извлечения контента из сложных документов. - -**Пример**: -```python -import mammoth - -with open("document.docx", "rb") as docx_file: - result = mammoth.convert_to_html(docx_file) - html = result.value # HTML-строка - warnings = result.messages # Предупреждения -``` - -**Плюсы**: -- Сохраняет семантику (заголовки, списки, таблицы). -- Легко интегрируется с парсерами (BeautifulSoup). - -**Минусы**: -- Не подходит для обратной конвертации (HTML → docx). - -**Ссылка**: [github.com/mwilliamson/mammoth.py](https://github.com/mwilliamson/mammoth.py) - ---- - -### **Низкоуровневые инструменты (для кастомных задач)** - -#### **1. zipfile + lxml** -**Назначение**: Прямой доступ к XML-структуре OOXML. -**Когда использовать**: -- При работе с нестандартными частями пакета (напр., кастомные XML-маппинги в Excel). -- Для восстановления поврежденных файлов. - -**Пример для Excel**: -```python -import zipfile -from lxml import etree - -# Распаковываем workbook.xml -with zipfile.ZipFile("report.xlsx") as xlsx: - xml_data = xlsx.read("xl/workbook.xml") - -# Парсим XML -workbook = etree.fromstring(xml_data) -namespaces = {"ns": "http://schemas.../spreadsheetml/2006/main"} -sheets = workbook.findall(".//ns:sheet", namespaces) - -# Меняем имя листа -sheets[0].set("{http://.../officeDocument/2006/relationships}name", "New Sheet") - -# Сохраняем изменения -with zipfile.ZipFile("fixed.xlsx", "w") as new_xlsx: - for file in xlsx.namelist(): - if file != "xl/workbook.xml": - new_xlsx.writestr(file, xlsx.read(file)) - new_xlsx.writestr("xl/workbook.xml", etree.tostring(workbook)) -``` - -**Плюсы**: -- Полный контроль над структурой. -- Работает с любыми частями пакета. - -**Минусы**: -- Требует знания XML-схем OOXML. -- Риск повреждения файла при ошибках. - ---- - -#### **2. officedoc** -**Назначение**: Универсальный доступ к форматам Office (docx, xlsx, pptx). -**Особенности**: -- Объединяет функционал `python-docx`, `openpyxl`, `python-pptx` в едином API. -- Упрощает обработку мультимедийных вложений. - -**Пример**: -```python -from officedoc import Document - -doc = Document("mixed.docx") -for image in doc.images: - image.save("extracted_" + image.filename) - -for table in doc.tables: - print(table.to_dataframe()) # Конвертация в pandas DataFrame -``` - -**Ссылка**: [github.com/mikem1701/officedoc](https://github.com/mikem1701/officedoc) - ---- - -### **Сравнение библиотек** - -| **Библиотека** | **Скорость** | **Сложность** | **Особенности** | -|----------------------|-------------|---------------|----------------------------| -| **python-docx** | Средняя | Средняя | Стандарт для работы с Word | -| **docxtpl** | Средняя | Низкая | Шаблонизация через Jinja2 | -| **zipfile + lxml** | Высокая | Высокая | Полный контроль над XML | - ---- - -#### Для Word (.docx) -- **Простое создание документов**: `python-docx`. -- **Шаблонные документы** (договоры, счета): `docxtpl`. -- **Извлечение контента**: `mammoth` → обработка HTML через BeautifulSoup. -- **Редактирование структуры**: `python-docx` + `zipfile` для кастомных частей. - -#### Для кастомных задач -- **Восстановление файлов**: `zipfile` + `lxml` (ручное исправление XML). -- **Интеграция с внешними системами**: `openpyxl`/`python-docx` + REST API (напр., выгрузка данных из БД в Excel). - ---- - -### Типичные проблемы и решения - -2. Стили не применяются в Word - - Проблема: `python-docx` использует стили из `styles.xml`, но не создает новые автоматически. - - Решение: - ```python - from docx.shared import Pt - style = doc.styles.add_style("Heading3", WD_STYLE_TYPE.PARAGRAPH) - font = style.font - font.name = "Arial" - font.size = Pt(12) - ``` - ---- - -### **Заключение** -Для 90% задач достаточно: -- **Excel**: `openpyxl` или `pandas`. -- **Word**: `python-docx` + `docxtpl`. - -**Используйте низкоуровневые методы (zipfile/lxml) только когда**: - -*Для углубленного изучения:* -- [Open XML SDK 2.5](https://learn.microsoft.com/ru-ru/office/open-xml/open-xml-sdk) (официальная документация Microsoft). -- [ECMA-376 стандарт](https://www.ecma-international.org/publications-and-standards/standards/ecma-376/) (полная спецификация). \ No newline at end of file +You can customize the application by modifying: +- `static/style.css` for visual styling +- `static/script.js` for client-side functionality +- `app.py` for server-side processing and document generation \ No newline at end of file diff --git a/app.py b/app.py new file mode 100644 index 0000000..26e2ade --- /dev/null +++ b/app.py @@ -0,0 +1,223 @@ +from flask import Flask, render_template, request, send_file, jsonify +import markdown +from docx import Document +from docx.shared import Inches, Pt +from docx.enum.text import WD_ALIGN_PARAGRAPH +from docx.enum.style import WD_STYLE_TYPE +from docx.oxml.ns import qn +import io +import json +import re +from werkzeug.utils import secure_filename + +app = Flask(__name__) + +# Global variables for tracking captions and table pagination +image_counter = 0 +table_counter = 0 + +def add_custom_styles(doc): + """Add custom styles to the document""" + # Heading 1 style + if "Heading 1" not in doc.styles: + h1 = doc.styles.add_style('Heading 1', WD_STYLE_TYPE.PARAGRAPH) + h1.font.name = 'Arial' + h1.font.size = Pt(16) + h1.font.bold = True + h1.paragraph_format.space_after = Pt(12) + + # Heading 2 style + if "Heading 2" not in doc.styles: + h2 = doc.styles.add_style('Heading 2', WD_STYLE_TYPE.PARAGRAPH) + h2.font.name = 'Arial' + h2.font.size = Pt(14) + h2.font.bold = True + h2.paragraph_format.space_after = Pt(10) + + # Heading 3 style + if "Heading 3" not in doc.styles: + h3 = doc.styles.add_style('Heading 3', WD_STYLE_TYPE.PARAGRAPH) + h3.font.name = 'Arial' + h3.font.size = Pt(12) + h3.font.bold = True + h3.paragraph_format.space_after = Pt(8) + + # Quote style + if "Quote" not in doc.styles: + quote = doc.styles.add_style('Quote', WD_STYLE_TYPE.PARAGRAPH) + quote.font.name = 'Arial' + quote.font.size = Pt(11) + quote.font.italic = True + quote.paragraph_format.left_indent = Inches(0.5) + quote.paragraph_format.right_indent = Inches(0.5) + quote.paragraph_format.line_spacing = 1.1 + +def convert_md_to_docx(markdown_text, config): + """Convert Markdown text to Word document with custom settings""" + global image_counter, table_counter + image_counter = 0 + table_counter = 0 + + # Create a new document + doc = Document() + + # Add custom styles + add_custom_styles(doc) + + # Parse markdown into lines + lines = markdown_text.split('\n') + + i = 0 + while i < len(lines): + line = lines[i].strip() + + if line.startswith('# '): + # Heading 1 + doc.add_heading(line[2:], level=0) + elif line.startswith('## '): + # Heading 2 + doc.add_heading(line[3:], level=1) + elif line.startswith('### '): + # Heading 3 + doc.add_heading(line[4:], level=2) + elif line.startswith('> '): + # Blockquote + p = doc.add_paragraph(line[2:], style='Quote') + elif line.startswith('|') and i + 1 < len(lines) and lines[i + 1].strip().startswith('|'): + # Table processing + table_lines = [] + while i < len(lines) and lines[i].strip().startswith('|'): + table_lines.append(lines[i].strip()) + i += 1 + + # Process the table + if table_lines: + headers = [cell.strip() for cell in table_lines[0].strip('|').split('|')] + separators = [cell.strip() for cell in table_lines[1].strip('|').split('|')] + rows = [] + + for j in range(2, len(table_lines)): + row = [cell.strip() for cell in table_lines[j].strip('|').split('|')] + rows.append(row) + + # Create table + table = doc.add_table(rows=1, cols=len(headers)) + table.style = 'Table Grid' + + # Add headers + hdr_cells = table.rows[0].cells + for j, header in enumerate(headers): + if j < len(hdr_cells): + hdr_cells[j].text = header + + # Add data rows + for row_data in rows: + row_cells = table.add_row().cells + for j, cell_data in enumerate(row_data): + if j < len(row_cells): + row_cells[j].text = cell_data + + # Add table caption if enabled + if config.get('autoCaptionTables', True): + table_counter += 1 + caption_para = doc.add_paragraph() + caption_run = caption_para.add_run(f'Table {table_counter}: ') + caption_run.italic = True + caption_run.font.size = Pt(10) + caption_run.font.name = 'Arial' + caption_para.alignment = WD_ALIGN_PARAGRAPH.CENTER + + elif line.startswith('```'): + # Code block + code_lines = [] + lang = line[3:] if len(line) > 3 else '' + i += 1 + while i < len(lines) and not lines[i].startswith('```'): + code_lines.append(lines[i]) + i += 1 + code_block = '\n'.join(code_lines) + + p = doc.add_paragraph() + p.alignment = WD_ALIGN_PARAGRAPH.LEFT + run = p.add_run(code_block) + run.font.name = 'Courier New' + run.font.size = Pt(10) + + # Apply shading to the paragraph - simplified approach + # For now, just add the code block as formatted text + pass + + elif line.startswith('- ') or line.startswith('* '): + # List item + p = doc.add_paragraph(line[2:], style='List Bullet') + elif line.startswith(' + match = re.match(r'!\[(.*?)\]\((.*?)\)', line) + if match: + alt_text = match.group(1) + img_url = match.group(2) + + # Add image if it's a valid local file + try: + # For this example, we'll just add a placeholder since we don't have actual image files + p = doc.add_paragraph('Image Placeholder: ' + alt_text) + p.alignment = WD_ALIGN_PARAGRAPH.CENTER + + # Add caption if enabled + if config.get('autoCaptionImages', True): + image_counter += 1 + caption_para = doc.add_paragraph() + caption_run = caption_para.add_run(f'Figure {image_counter}: {alt_text}') + caption_run.italic = True + caption_run.font.size = Pt(10) + caption_run.font.name = 'Arial' + caption_para.alignment = WD_ALIGN_PARAGRAPH.CENTER + + except: + # If image can't be added, just add alt text + doc.add_paragraph(alt_text) + elif line == '': + # Empty line - add spacing + doc.add_paragraph('') + else: + # Regular paragraph + if line: + doc.add_paragraph(line) + + i += 1 + + return doc + +@app.route('/') +def index(): + return render_template('index.html') + +@app.route('/export', methods=['POST']) +def export(): + markdown_text = request.form.get('markdown', '') + config_json = request.form.get('config', '{}') + + try: + config = json.loads(config_json) + except: + config = {} + + # Convert markdown to docx + doc = convert_md_to_docx(markdown_text, config) + + # Save to BytesIO object + file_stream = io.BytesIO() + doc.save(file_stream) + file_stream.seek(0) + + # Return the file for download + return send_file( + file_stream, + as_attachment=True, + download_name='document.docx', + mimetype='application/vnd.openxmlformats-officedocument.wordprocessingml.document' + ) + +if __name__ == '__main__': + app.run(debug=True, host='0.0.0.0', port=5000) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..a9270b7 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +Flask==2.3.3 +python-docx==0.8.11 +markdown==3.4.4 \ No newline at end of file diff --git a/static/script.js b/static/script.js new file mode 100644 index 0000000..6e1e475 --- /dev/null +++ b/static/script.js @@ -0,0 +1,298 @@ +// GhostEditor - Markdown to Word Converter JavaScript +document.addEventListener('DOMContentLoaded', function() { + // DOM Elements + const markdownInput = document.getElementById('markdownInput'); + const previewOutput = document.getElementById('previewOutput'); + const previewToggle = document.getElementById('previewToggle'); + const exportBtn = document.getElementById('exportBtn'); + const blockType = document.getElementById('blockType'); + const applyStyle = document.getElementById('applyStyle'); + const applyStyleBtn = document.getElementById('applyStyleBtn'); + const customCaption = document.getElementById('customCaption'); + const applyCaptionBtn = document.getElementById('applyCaptionBtn'); + const autoCaptionImages = document.getElementById('autoCaptionImages'); + const autoCaptionTables = document.getElementById('autoCaptionTables'); + const autoCaptionFigures = document.getElementById('autoCaptionFigures'); + const tablePagination = document.getElementById('tablePagination'); + const boldBtn = document.getElementById('boldBtn'); + const italicBtn = document.getElementById('italicBtn'); + const headingBtn = document.getElementById('headingBtn'); + const listBtn = document.getElementById('listBtn'); + const imageBtn = document.getElementById('imageBtn'); + + // Configuration + let config = { + autoCaptionImages: true, + autoCaptionTables: true, + autoCaptionFigures: true, + tablePagination: true, + defaultStyle: 'Normal' + }; + + // Initialize with sample markdown + markdownInput.value = `# GhostEditor Sample Document + +This is a sample document to demonstrate the real-time conversion capabilities of GhostEditor. + +## Features Demonstrated + +### Auto-captioning + +*Figure 1: Sample placeholder image* + +### Table with Pagination Support +| ID | Name | Role | +|----|------|------| +| 1 | John | Developer | +| 2 | Jane | Designer | +| 3 | Bob | Manager | +| 4 | Alice | Analyst | + +### Code Block +\`\`\`python +def hello_world(): + print("Hello, GhostEditor!") +\`\`\` + +### Blockquote +> This is a sample quote demonstrating the styling capabilities of GhostEditor. + +### List +- First item +- Second item +- Third item`; + + // Update configuration when settings change + autoCaptionImages.addEventListener('change', updateConfig); + autoCaptionTables.addEventListener('change', updateConfig); + autoCaptionFigures.addEventListener('change', updateConfig); + tablePagination.addEventListener('change', updateConfig); + + // Toolbar buttons + boldBtn.addEventListener('click', () => wrapText('**', '**')); + italicBtn.addEventListener('click', () => wrapText('*', '*')); + headingBtn.addEventListener('click', () => wrapLine('# ')); + listBtn.addEventListener('click', () => wrapLine('- ')); + imageBtn.addEventListener('click', () => insertImage()); + + // Apply style button + applyStyleBtn.addEventListener('click', applyStyleToSelection); + + // Apply caption button + applyCaptionBtn.addEventListener('click', applyCaptionToSelection); + + // Export button + exportBtn.addEventListener('click', exportToWord); + + // Initial preview update + updatePreview(); + + // Update preview when input changes + markdownInput.addEventListener('input', updatePreview); + + // Update preview when toggle changes + previewToggle.addEventListener('change', function() { + if (this.checked) { + updatePreview(); + } else { + previewOutput.innerHTML = '
Preview is disabled. Enable to see real-time conversion.
'; + } + }); + + // Update configuration + function updateConfig() { + config = { + autoCaptionImages: autoCaptionImages.checked, + autoCaptionTables: autoCaptionTables.checked, + autoCaptionFigures: autoCaptionFigures.checked, + tablePagination: tablePagination.checked, + defaultStyle: document.getElementById('defaultStyle').value + }; + + // Update preview after config change + updatePreview(); + } + + // Update preview function + function updatePreview() { + if (!previewToggle.checked) return; + + const markdown = markdownInput.value; + + // Convert markdown to HTML using Marked.js + const html = marked.parse(markdown); + + // Process the HTML to add captions and table pagination + const processedHtml = processHtmlForPreview(html); + + previewOutput.innerHTML = processedHtml; + + // Add captions to images if enabled + if (config.autoCaptionImages) { + addImageCaptions(); + } + } + + // Process HTML for preview with special features + function processHtmlForPreview(html) { + let processed = html; + + // Add table pagination header if enabled + if (config.tablePagination) { + processed = processed.replace(/