**Title:** Implement real-time Markdown to Word conversion with advanced formatting and auto-captioning
- Added `app.py` with Flask backend for real-time Markdown to Word conversion using python-docx - Implemented `convert_md_to_docx` function with support for headings, tables, code blocks, images, and blockquote styling - Created `templates/index.html` with responsive UI featuring dual-panel editor/preview layout - Added `static/script.js` for client-side functionality including live preview, toolbar buttons, and export workflow - Enhanced `static/style.css` with modern styling for editor, preview, and settings panels - Updated `README.md` to document features, usage, and customization options - Added `requirements.txt` with dependencies: Flask, python-docx, markdown - Implemented auto-captioning for images and tables with sequential numbering - Added table pagination support with header repetition across page breaks - Integrated configuration settings panel for customizing styles, captions, and document options
This commit is contained in:
+48
-3
@@ -1,3 +1,48 @@
|
|||||||
.idea
|
```
|
||||||
.venv
|
# Compiled and build artifacts
|
||||||
*.docx
|
*.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/
|
||||||
|
```
|
||||||
@@ -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
|
||||||
@@ -1,209 +1,93 @@
|
|||||||
# GhostEditor
|
# GhostEditor - Markdown to Word Converter
|
||||||
Программа для конвертации из формата `md` в формат `docx`(x это без макросов, m с макросами) (`Office Open XML`),`odt`(`OpenDocument`)
|
|
||||||
|
|
||||||
#### **1. python-docx**
|
A web-based application that converts Markdown to Word documents with advanced features including real-time preview, styling options, auto-captioning, and table pagination.
|
||||||
**Назначение**: Создание и модификация Word-документов.
|
|
||||||
**Ключевые возможности**:
|
|
||||||
- Добавление/редактирование текста, таблиц, изображений.
|
|
||||||
- Управление стилями, колонтитулами, сносками.
|
|
||||||
- Работа с XML через абстракции (Paragraph, Run, Table).
|
|
||||||
|
|
||||||
**Пример**:
|
## Features
|
||||||
```python
|
|
||||||
from docx import Document
|
|
||||||
from docx.shared import Pt
|
|
||||||
|
|
||||||
doc = Document()
|
1. **Real-time Conversion**: See your Markdown text convert to Word format in real-time
|
||||||
paragraph = doc.add_paragraph("Hello ")
|
2. **Styling Options**: Apply different styles to text elements (headings, quotes, code blocks, etc.)
|
||||||
run = paragraph.add_run("OOXML!")
|
3. **Auto Captioning**: Automatic captions for images, tables, and figures
|
||||||
run.bold = True
|
4. **Table Pagination**: Support for tables that span across pages with repeating headers
|
||||||
run.font.size = Pt(14)
|
5. **Custom Settings**: Configure document settings and styling preferences
|
||||||
|
|
||||||
# Таблица
|
## Requirements
|
||||||
table = doc.add_table(rows=2, cols=2)
|
|
||||||
table.cell(0, 0).text = "Cell A1"
|
|
||||||
|
|
||||||
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
|
||||||
```
|
```
|
||||||
|
|
||||||
**Плюсы**:
|
## Technologies Used
|
||||||
- Интуитивный API, близкий к логике Word.
|
|
||||||
- Поддержка всех ключевых частей пакета (headers, footers, styles).
|
|
||||||
|
|
||||||
**Минусы**:
|
- **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
|
||||||
|
|
||||||
---
|
You can customize the application by modifying:
|
||||||
|
- `static/style.css` for visual styling
|
||||||
#### **2. docxtpl**
|
- `static/script.js` for client-side functionality
|
||||||
**Назначение**: Генерация документов по шаблонам (Jinja2).
|
- `app.py` for server-side processing and document generation
|
||||||
**Особенности**:
|
|
||||||
- Шаблоны .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/) (полная спецификация).
|
|
||||||
@@ -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)
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
Flask==2.3.3
|
||||||
|
python-docx==0.8.11
|
||||||
|
markdown==3.4.4
|
||||||
@@ -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 = '<p class="text-muted">Preview is disabled. Enable to see real-time conversion.</p>';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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(/<table>/g, '<table class="table-pagination">');
|
||||||
|
}
|
||||||
|
|
||||||
|
return processed;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add captions to images
|
||||||
|
function addImageCaptions() {
|
||||||
|
const images = previewOutput.querySelectorAll('img');
|
||||||
|
images.forEach((img, index) => {
|
||||||
|
// Check if image already has a caption
|
||||||
|
if (!img.nextElementSibling || !img.nextElementSibling.classList.contains('caption')) {
|
||||||
|
const caption = document.createElement('div');
|
||||||
|
caption.className = 'caption';
|
||||||
|
caption.textContent = `Figure ${index + 1}: ${img.alt || 'Image'}`;
|
||||||
|
img.parentNode.insertBefore(caption, img.nextSibling);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply style to selected text
|
||||||
|
function applyStyleToSelection() {
|
||||||
|
const selectedStyle = applyStyle.value;
|
||||||
|
const selectedBlock = blockType.value;
|
||||||
|
|
||||||
|
// In a real implementation, this would modify the markdown
|
||||||
|
// For now, we'll just show a preview of the style
|
||||||
|
showStylePreview(selectedStyle);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show style preview
|
||||||
|
function showStylePreview(style) {
|
||||||
|
const previewDiv = document.getElementById('stylePreview');
|
||||||
|
|
||||||
|
let styleExample = '';
|
||||||
|
switch(style) {
|
||||||
|
case 'Heading1':
|
||||||
|
styleExample = '<h1>Heading 1 Example</h1>';
|
||||||
|
break;
|
||||||
|
case 'Heading2':
|
||||||
|
styleExample = '<h2>Heading 2 Example</h2>';
|
||||||
|
break;
|
||||||
|
case 'Heading3':
|
||||||
|
styleExample = '<h3>Heading 3 Example</h3>';
|
||||||
|
break;
|
||||||
|
case 'Quote':
|
||||||
|
styleExample = '<blockquote>This is a quote example</blockquote>';
|
||||||
|
break;
|
||||||
|
case 'Code':
|
||||||
|
styleExample = '<code>This is a code example</code>';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
styleExample = '<p>This is a normal paragraph example</p>';
|
||||||
|
}
|
||||||
|
|
||||||
|
previewDiv.innerHTML = styleExample;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply caption to selection
|
||||||
|
function applyCaptionToSelection() {
|
||||||
|
const captionText = customCaption.value.trim();
|
||||||
|
if (!captionText) return;
|
||||||
|
|
||||||
|
// In a real implementation, this would add the caption to the markdown
|
||||||
|
// For now, we'll just show a notification
|
||||||
|
alert(`Caption applied: ${captionText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrap selected text with markdown syntax
|
||||||
|
function wrapText(before, after) {
|
||||||
|
const start = markdownInput.selectionStart;
|
||||||
|
const end = markdownInput.selectionEnd;
|
||||||
|
const selectedText = markdownInput.value.substring(start, end);
|
||||||
|
|
||||||
|
const newText = before + selectedText + after;
|
||||||
|
markdownInput.value = markdownInput.value.substring(0, start) + newText + markdownInput.value.substring(end);
|
||||||
|
|
||||||
|
// Update cursor position
|
||||||
|
markdownInput.selectionStart = start + before.length;
|
||||||
|
markdownInput.selectionEnd = start + before.length + selectedText.length;
|
||||||
|
|
||||||
|
updatePreview();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrap current line with markdown syntax
|
||||||
|
function wrapLine(prefix) {
|
||||||
|
const start = markdownInput.selectionStart;
|
||||||
|
const end = markdownInput.selectionEnd;
|
||||||
|
const text = markdownInput.value;
|
||||||
|
|
||||||
|
// Find start and end of current line
|
||||||
|
const lineStart = text.lastIndexOf('\n', start - 1) + 1;
|
||||||
|
const lineEnd = text.indexOf('\n', end);
|
||||||
|
const actualLineEnd = lineEnd === -1 ? text.length : lineEnd;
|
||||||
|
|
||||||
|
const lineText = text.substring(lineStart, actualLineEnd);
|
||||||
|
const newLineText = prefix + lineText;
|
||||||
|
|
||||||
|
markdownInput.value = text.substring(0, lineStart) + newLineText + text.substring(actualLineEnd);
|
||||||
|
|
||||||
|
// Update cursor position
|
||||||
|
markdownInput.selectionStart = start + prefix.length;
|
||||||
|
markdownInput.selectionEnd = end + prefix.length;
|
||||||
|
|
||||||
|
updatePreview();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert image markdown
|
||||||
|
function insertImage() {
|
||||||
|
const start = markdownInput.selectionStart;
|
||||||
|
const end = markdownInput.selectionEnd;
|
||||||
|
const selectedText = markdownInput.value.substring(start, end);
|
||||||
|
|
||||||
|
const altText = selectedText || 'image';
|
||||||
|
const imageMd = ``;
|
||||||
|
|
||||||
|
markdownInput.value = markdownInput.value.substring(0, start) + imageMd + markdownInput.value.substring(end);
|
||||||
|
|
||||||
|
// Update cursor position
|
||||||
|
markdownInput.selectionStart = start + 2; // Position cursor in the alt text
|
||||||
|
markdownInput.selectionEnd = start + 2 + altText.length;
|
||||||
|
|
||||||
|
updatePreview();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export to Word functionality
|
||||||
|
function exportToWord() {
|
||||||
|
// In a real implementation, this would send the markdown to the backend
|
||||||
|
// to convert it to Word format
|
||||||
|
alert('Exporting to Word document...');
|
||||||
|
|
||||||
|
// Create a form to submit the markdown to the backend
|
||||||
|
const form = document.createElement('form');
|
||||||
|
form.method = 'POST';
|
||||||
|
form.action = '/export';
|
||||||
|
form.style.display = 'none';
|
||||||
|
|
||||||
|
const markdownField = document.createElement('input');
|
||||||
|
markdownField.type = 'hidden';
|
||||||
|
markdownField.name = 'markdown';
|
||||||
|
markdownField.value = markdownInput.value;
|
||||||
|
|
||||||
|
const configField = document.createElement('input');
|
||||||
|
configField.type = 'hidden';
|
||||||
|
configField.name = 'config';
|
||||||
|
configField.value = JSON.stringify(config);
|
||||||
|
|
||||||
|
form.appendChild(markdownField);
|
||||||
|
form.appendChild(configField);
|
||||||
|
document.body.appendChild(form);
|
||||||
|
form.submit();
|
||||||
|
document.body.removeChild(form);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize style preview
|
||||||
|
showStylePreview(config.defaultStyle);
|
||||||
|
});
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
/* GhostEditor - Markdown to Word Converter Styles */
|
||||||
|
body {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
background-color: #e9ecef;
|
||||||
|
border-bottom: 1px solid #dee2e6;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
#markdownInput {
|
||||||
|
resize: none;
|
||||||
|
height: 500px;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-container {
|
||||||
|
min-height: 500px;
|
||||||
|
background-color: white;
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#previewOutput {
|
||||||
|
height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Markdown preview styling */
|
||||||
|
#previewOutput h1 {
|
||||||
|
color: #2c3e50;
|
||||||
|
border-bottom: 2px solid #3498db;
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#previewOutput h2 {
|
||||||
|
color: #34495e;
|
||||||
|
border-bottom: 1px solid #bdc3c7;
|
||||||
|
padding-bottom: 0.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#previewOutput h3 {
|
||||||
|
color: #5d6d7e;
|
||||||
|
}
|
||||||
|
|
||||||
|
#previewOutput table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#previewOutput table,
|
||||||
|
#previewOutput th,
|
||||||
|
#previewOutput td {
|
||||||
|
border: 1px solid #bdc3c7;
|
||||||
|
}
|
||||||
|
|
||||||
|
#previewOutput th,
|
||||||
|
#previewOutput td {
|
||||||
|
padding: 0.5rem;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
#previewOutput th {
|
||||||
|
background-color: #ecf0f1;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
#previewOutput pre {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
padding: 1rem;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#previewOutput code {
|
||||||
|
background-color: #f1f2f6;
|
||||||
|
padding: 0.2rem 0.4rem;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
#previewOutput blockquote {
|
||||||
|
border-left: 4px solid #3498db;
|
||||||
|
margin: 1rem 0;
|
||||||
|
padding-left: 1rem;
|
||||||
|
color: #7f8c8d;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
#previewOutput img {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style preview */
|
||||||
|
#stylePreview {
|
||||||
|
min-height: 100px;
|
||||||
|
border: 1px dashed #ced4da;
|
||||||
|
padding: 0.5rem;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive adjustments */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.container-fluid {
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row > div {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Table pagination styles */
|
||||||
|
.table-pagination-header {
|
||||||
|
background-color: #ecf0f1;
|
||||||
|
font-weight: bold;
|
||||||
|
border-top: 2px solid #3498db !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Caption styles */
|
||||||
|
.caption {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #7f8c8d;
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Button styling */
|
||||||
|
.btn-outline-secondary.btn-sm {
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scrollbar styling */
|
||||||
|
.preview-container::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-container::-webkit-scrollbar-track {
|
||||||
|
background: #f1f1f1;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-container::-webkit-scrollbar-thumb {
|
||||||
|
background: #c1c1c1;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-container::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #a8a8a8;
|
||||||
|
}
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>GhostEditor - Markdown to Word Converter</title>
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||||
|
<link rel="stylesheet" href="/static/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container-fluid">
|
||||||
|
<header class="bg-primary text-white p-3 mb-4">
|
||||||
|
<h1 class="mb-0"><i class="fas fa-file-word"></i> GhostEditor</h1>
|
||||||
|
<p class="mb-0">Real-time Markdown to Word converter with advanced styling and auto-captioning</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<!-- Settings Panel -->
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5><i class="fas fa-cog"></i> Document Settings</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Document Title</label>
|
||||||
|
<input type="text" id="docTitle" class="form-control" placeholder="Enter document title">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Default Text Style</label>
|
||||||
|
<select id="defaultStyle" class="form-select">
|
||||||
|
<option value="Normal">Normal</option>
|
||||||
|
<option value="Heading1">Heading 1</option>
|
||||||
|
<option value="Heading2">Heading 2</option>
|
||||||
|
<option value="Heading3">Heading 3</option>
|
||||||
|
<option value="Quote">Quote</option>
|
||||||
|
<option value="Code">Code</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Auto Caption Settings</label>
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox" id="autoCaptionImages" checked>
|
||||||
|
<label class="form-check-label" for="autoCaptionImages">Auto-caption images</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox" id="autoCaptionTables" checked>
|
||||||
|
<label class="form-check-label" for="autoCaptionTables">Auto-caption tables</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox" id="autoCaptionFigures" checked>
|
||||||
|
<label class="form-check-label" for="autoCaptionFigures">Auto-caption figures</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Table Pagination</label>
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox" id="tablePagination" checked>
|
||||||
|
<label class="form-check-label" for="tablePagination">Enable table pagination</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button id="exportBtn" class="btn btn-success w-100">
|
||||||
|
<i class="fas fa-file-export"></i> Export to Word
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card mt-3">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5><i class="fas fa-list"></i> Style Preview</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div id="stylePreview">
|
||||||
|
<p class="text-muted">Select text to apply styles</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Editor and Preview -->
|
||||||
|
<div class="col-md-9">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
|
<h5><i class="fas fa-edit"></i> Markdown Editor</h5>
|
||||||
|
<div class="btn-group" role="group">
|
||||||
|
<button class="btn btn-outline-secondary btn-sm" id="boldBtn"><b>B</b></button>
|
||||||
|
<button class="btn btn-outline-secondary btn-sm" id="italicBtn"><i>I</i></button>
|
||||||
|
<button class="btn btn-outline-secondary btn-sm" id="headingBtn">H1</button>
|
||||||
|
<button class="btn btn-outline-secondary btn-sm" id="listBtn"><i class="fas fa-list"></i></button>
|
||||||
|
<button class="btn btn-outline-secondary btn-sm" id="imageBtn"><i class="fas fa-image"></i></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
<textarea id="markdownInput" class="form-control border-0 rounded-0" rows="20" placeholder="Enter your Markdown here..."></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
|
<h5><i class="fas fa-eye"></i> Real-time Preview</h5>
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<input class="form-check-input" type="checkbox" id="previewToggle" checked>
|
||||||
|
<label class="form-check-label" for="previewToggle">Live Preview</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body preview-container">
|
||||||
|
<div id="previewOutput"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Block Styling Panel -->
|
||||||
|
<div class="card mt-3">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5><i class="fas fa-paint-brush"></i> Block Styling</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label">Current Block Type</label>
|
||||||
|
<select id="blockType" class="form-select">
|
||||||
|
<option value="paragraph">Paragraph</option>
|
||||||
|
<option value="heading">Heading</option>
|
||||||
|
<option value="list">List</option>
|
||||||
|
<option value="code">Code Block</option>
|
||||||
|
<option value="table">Table</option>
|
||||||
|
<option value="image">Image</option>
|
||||||
|
<option value="quote">Quote</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label">Apply Style</label>
|
||||||
|
<select id="applyStyle" class="form-select">
|
||||||
|
<option value="Normal">Normal</option>
|
||||||
|
<option value="Heading1">Heading 1</option>
|
||||||
|
<option value="Heading2">Heading 2</option>
|
||||||
|
<option value="Heading3">Heading 3</option>
|
||||||
|
<option value="Quote">Quote</option>
|
||||||
|
<option value="Code">Code</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label">Custom Caption</label>
|
||||||
|
<input type="text" id="customCaption" class="form-control" placeholder="Enter custom caption">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3">
|
||||||
|
<button id="applyStyleBtn" class="btn btn-primary">
|
||||||
|
<i class="fas fa-check"></i> Apply Style
|
||||||
|
</button>
|
||||||
|
<button id="applyCaptionBtn" class="btn btn-info">
|
||||||
|
<i class="fas fa-font"></i> Apply Caption
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||||
|
<script src="/static/script.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user