**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
|
||||
*.docx
|
||||
```
|
||||
# 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/
|
||||
```
|
||||
@@ -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
|
||||
Программа для конвертации из формата `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/) (полная спецификация).
|
||||
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
|
||||
@@ -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