Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 144bfb2ba7 | |||
| 368426f0f4 |
+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,324 @@
|
|||||||
|
from flask import Flask, render_template, request, send_file, jsonify
|
||||||
|
import markdown
|
||||||
|
from odfdo import Document, Paragraph, Header, ListItem, List, Table, Row as TableRow, Cell as TableCell, Span, Style, Frame, Image
|
||||||
|
import io
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import requests
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
ALLOWED_IMAGE_DOMAINS = ["upload.wikimedia.org"]
|
||||||
|
|
||||||
|
# Load styles from file or create default styles
|
||||||
|
try:
|
||||||
|
with open('styles.json', 'r') as f:
|
||||||
|
styles = json.load(f)
|
||||||
|
except FileNotFoundError:
|
||||||
|
styles = {
|
||||||
|
"Normal": {
|
||||||
|
"font_size": "12pt",
|
||||||
|
"font_family": "Liberation Serif",
|
||||||
|
"margin_bottom": "0.1in"
|
||||||
|
},
|
||||||
|
"Heading 1": {
|
||||||
|
"font_size": "18pt",
|
||||||
|
"font_family": "Liberation Sans",
|
||||||
|
"font_weight": "bold",
|
||||||
|
"margin_bottom": "0.2in"
|
||||||
|
},
|
||||||
|
"Heading 2": {
|
||||||
|
"font_size": "14pt",
|
||||||
|
"font_family": "Liberation Sans",
|
||||||
|
"font_weight": "bold",
|
||||||
|
"margin_bottom": "0.15in"
|
||||||
|
},
|
||||||
|
"Caption": {
|
||||||
|
"font_size": "10pt",
|
||||||
|
"font_family": "Liberation Serif",
|
||||||
|
"font_style": "italic",
|
||||||
|
"text_align": "center"
|
||||||
|
},
|
||||||
|
"Code": {
|
||||||
|
"font_family": "Courier New",
|
||||||
|
"font_size": "10pt",
|
||||||
|
"background_color": "#f0f0f0",
|
||||||
|
"padding": "10px"
|
||||||
|
},
|
||||||
|
"Quote": {
|
||||||
|
"font_style": "italic",
|
||||||
|
"border_left": "3px solid #ccc",
|
||||||
|
"padding_left": "10px"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
with open('styles.json', 'w') as f:
|
||||||
|
json.dump(styles, f, indent=4)
|
||||||
|
|
||||||
|
@app.route('/styles', methods=['GET'])
|
||||||
|
def get_styles():
|
||||||
|
return jsonify(styles)
|
||||||
|
|
||||||
|
@app.route('/styles/<style_name>', methods=['GET', 'POST'])
|
||||||
|
def manage_style(style_name):
|
||||||
|
if request.method == 'POST':
|
||||||
|
data = request.json
|
||||||
|
styles[style_name] = data
|
||||||
|
with open('styles.json', 'w') as f:
|
||||||
|
json.dump(styles, f, indent=4)
|
||||||
|
return jsonify({"message": f"Style '{style_name}' updated successfully."})
|
||||||
|
else:
|
||||||
|
return jsonify(styles.get(style_name, {}))
|
||||||
|
|
||||||
|
def generate_toc(markdown_text):
|
||||||
|
toc = []
|
||||||
|
for line in markdown_text.splitlines():
|
||||||
|
if line.startswith('# '):
|
||||||
|
toc.append((1, line[2:].strip()))
|
||||||
|
elif line.startswith('## '):
|
||||||
|
toc.append((2, line[3:].strip()))
|
||||||
|
return toc
|
||||||
|
|
||||||
|
def apply_styles_to_doc(doc):
|
||||||
|
for name, properties in styles.items():
|
||||||
|
style = Style(
|
||||||
|
'paragraph',
|
||||||
|
name=name,
|
||||||
|
**{
|
||||||
|
"fo:font-size": properties.get("font_size"),
|
||||||
|
"fo:font-family": properties.get("font_family"),
|
||||||
|
"fo:font-weight": properties.get("font_weight"),
|
||||||
|
"fo:font-style": properties.get("font_style"),
|
||||||
|
"fo:text-align": properties.get("text_align"),
|
||||||
|
"fo:border-left": properties.get("border_left"),
|
||||||
|
"fo:padding-left": properties.get("padding_left"),
|
||||||
|
"fo:background-color": properties.get("background_color"),
|
||||||
|
"fo:padding": properties.get("padding"),
|
||||||
|
"fo:margin-bottom": properties.get("margin_bottom")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
doc.insert_style(style, automatic=True)
|
||||||
|
|
||||||
|
def convert_md_to_odt(markdown_text, applied_styles, max_rows=10):
|
||||||
|
doc = Document('text')
|
||||||
|
apply_styles_to_doc(doc)
|
||||||
|
body = doc.body
|
||||||
|
table_counter = 0
|
||||||
|
image_counter = 0
|
||||||
|
|
||||||
|
lines = markdown_text.splitlines()
|
||||||
|
i = 0
|
||||||
|
toc_placeholder_index = -1
|
||||||
|
for idx, line in enumerate(lines):
|
||||||
|
if "[TOC]" in line:
|
||||||
|
toc_placeholder_index = idx
|
||||||
|
break
|
||||||
|
|
||||||
|
headings = generate_toc(markdown_text)
|
||||||
|
|
||||||
|
i = 0
|
||||||
|
while i < len(lines):
|
||||||
|
line = lines[i]
|
||||||
|
style_name = applied_styles.get(str(i), "Normal")
|
||||||
|
|
||||||
|
if i == toc_placeholder_index:
|
||||||
|
toc_list = List()
|
||||||
|
for level, text in headings:
|
||||||
|
item = ListItem()
|
||||||
|
p = Paragraph()
|
||||||
|
bookmark_name = f"__RefHeading_{hash(text)}"
|
||||||
|
link = Span(text)
|
||||||
|
link.set_attribute('xlink:href', f"#{bookmark_name}")
|
||||||
|
link.set_attribute('xlink:type', 'simple')
|
||||||
|
p.append(link)
|
||||||
|
item.append(p)
|
||||||
|
toc_list.append(item)
|
||||||
|
body.append(toc_list)
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
if line.startswith('# '):
|
||||||
|
text = line[2:].strip()
|
||||||
|
h = Header(1, text=text)
|
||||||
|
h.style = style_name
|
||||||
|
bookmark_name = f"__RefHeading_{hash(text)}"
|
||||||
|
h.set_bookmark(bookmark_name)
|
||||||
|
body.append(h)
|
||||||
|
elif line.startswith('## '):
|
||||||
|
text = line[3:].strip()
|
||||||
|
h = Header(2, text=text)
|
||||||
|
h.style = style_name
|
||||||
|
bookmark_name = f"__RefHeading_{hash(text)}"
|
||||||
|
h.set_bookmark(bookmark_name)
|
||||||
|
body.append(h)
|
||||||
|
elif line.startswith('- '):
|
||||||
|
odt_list = List()
|
||||||
|
while i < len(lines) and lines[i].strip().startswith('- '):
|
||||||
|
item_style = applied_styles.get(str(i), "Normal")
|
||||||
|
odt_list.append(ListItem(Paragraph(text=lines[i].strip()[2:], style=item_style)))
|
||||||
|
i += 1
|
||||||
|
body.append(odt_list)
|
||||||
|
i -= 1
|
||||||
|
elif line.startswith('|'):
|
||||||
|
table_lines = []
|
||||||
|
while i < len(lines) and lines[i].strip().startswith('|'):
|
||||||
|
table_lines.append(lines[i].strip())
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
if len(table_lines) > 1:
|
||||||
|
headers = [h.strip() for h in table_lines[0].strip('|').split('|')]
|
||||||
|
table_counter += 1
|
||||||
|
|
||||||
|
rows = table_lines[2:]
|
||||||
|
num_rows = len(rows)
|
||||||
|
|
||||||
|
for j in range(0, num_rows, max_rows):
|
||||||
|
chunk = rows[j:j + max_rows]
|
||||||
|
|
||||||
|
chunk_table = Table(name=f"Table{table_counter}_{j}", width=len(headers))
|
||||||
|
|
||||||
|
header_row = TableRow()
|
||||||
|
for header in headers:
|
||||||
|
cell = TableCell()
|
||||||
|
cell.append(Paragraph(text=header))
|
||||||
|
header_row.append(cell)
|
||||||
|
chunk_table.append(header_row)
|
||||||
|
|
||||||
|
for row_line in chunk:
|
||||||
|
row_cells = [c.strip() for c in row_line.strip('|').split('|')]
|
||||||
|
row = TableRow()
|
||||||
|
for cell_text in row_cells:
|
||||||
|
cell = TableCell()
|
||||||
|
cell.append(Paragraph(text=cell_text))
|
||||||
|
row.append(cell)
|
||||||
|
chunk_table.append(row)
|
||||||
|
body.append(chunk_table)
|
||||||
|
|
||||||
|
caption_text = f"Table {table_counter}"
|
||||||
|
if j > 0:
|
||||||
|
caption_text += " (continued)"
|
||||||
|
caption = Paragraph(caption_text, style="Caption")
|
||||||
|
body.append(caption)
|
||||||
|
elif line.startswith('!['):
|
||||||
|
match = re.match(r'!\[(.*?)\]\((.*?)\)', line)
|
||||||
|
if match:
|
||||||
|
alt_text = match.group(1)
|
||||||
|
image_url = match.group(2)
|
||||||
|
|
||||||
|
try:
|
||||||
|
domain = urlparse(image_url).netloc
|
||||||
|
if domain not in ALLOWED_IMAGE_DOMAINS:
|
||||||
|
raise ValueError("Image domain not allowed.")
|
||||||
|
|
||||||
|
response = requests.get(image_url, stream=True, timeout=10)
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
# Check for content length
|
||||||
|
content_length = response.headers.get('content-length')
|
||||||
|
if content_length and int(content_length) > 10 * 1024 * 1024:
|
||||||
|
raise ValueError("Image file is too large.")
|
||||||
|
|
||||||
|
with tempfile.NamedTemporaryFile(delete=False, suffix=".jpg") as temp_file:
|
||||||
|
for chunk in response.iter_content(chunk_size=8192):
|
||||||
|
temp_file.write(chunk)
|
||||||
|
|
||||||
|
# Add the image to the document
|
||||||
|
image_data = doc.add_file(temp_file.name)
|
||||||
|
|
||||||
|
# Create a frame and insert the image
|
||||||
|
frame = Frame(width="10cm", height="10cm")
|
||||||
|
frame.append(Image(url=image_data))
|
||||||
|
body.append(frame)
|
||||||
|
|
||||||
|
# Add the caption
|
||||||
|
image_counter += 1
|
||||||
|
caption = Paragraph(f"Figure {image_counter}: {alt_text}", style="Caption")
|
||||||
|
body.append(caption)
|
||||||
|
|
||||||
|
# Clean up the temporary file
|
||||||
|
os.unlink(temp_file.name)
|
||||||
|
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
p = Paragraph(f"[Image not found: {alt_text}]", style="Normal")
|
||||||
|
body.append(p)
|
||||||
|
elif line.startswith('```'):
|
||||||
|
code_lines = []
|
||||||
|
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 = Paragraph(code_block, style="Code")
|
||||||
|
body.append(p)
|
||||||
|
elif line.startswith('> '):
|
||||||
|
p = Paragraph(line[2:], style="Quote")
|
||||||
|
body.append(p)
|
||||||
|
elif line:
|
||||||
|
p = Paragraph(style=style_name)
|
||||||
|
parts = re.split(r'(\\*\\*.*\\*\\*|\\*.*\\*)', line)
|
||||||
|
for part in parts:
|
||||||
|
if part.startswith('**') and part.endswith('**'):
|
||||||
|
p.append(Span(part[2:-2], style='bold'))
|
||||||
|
elif part.startswith('*') and part.endswith('*'):
|
||||||
|
p.append(Span(part[1:-1], style='italic'))
|
||||||
|
else:
|
||||||
|
p.append(part)
|
||||||
|
body.append(p)
|
||||||
|
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
return doc
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
def index():
|
||||||
|
return render_template('index.html')
|
||||||
|
|
||||||
|
@app.route('/preview', methods=['POST'])
|
||||||
|
def preview():
|
||||||
|
data = request.json
|
||||||
|
markdown_text = data.get('markdown', '')
|
||||||
|
applied_styles = data.get('styles', {})
|
||||||
|
|
||||||
|
# Generate CSS from styles
|
||||||
|
css = ""
|
||||||
|
for name, properties in styles.items():
|
||||||
|
css += f".style-{name.replace(' ', '-')} {{"
|
||||||
|
for prop, value in properties.items():
|
||||||
|
css += f"{prop.replace('_', '-')}: {value};"
|
||||||
|
css += "}"
|
||||||
|
|
||||||
|
# Generate styled HTML
|
||||||
|
html = markdown.markdown(markdown_text, extensions=['tables'])
|
||||||
|
lines = html.splitlines()
|
||||||
|
styled_html = ""
|
||||||
|
for i, line in enumerate(lines):
|
||||||
|
style_name = applied_styles.get(str(i), "Normal").replace(' ', '-')
|
||||||
|
styled_html += f"<div class='style-{style_name}'>{line}</div>"
|
||||||
|
|
||||||
|
full_html = f"<style>{css}</style>{styled_html}"
|
||||||
|
|
||||||
|
return jsonify({'html': full_html})
|
||||||
|
|
||||||
|
@app.route('/export', methods=['POST'])
|
||||||
|
def export():
|
||||||
|
data = request.json
|
||||||
|
markdown_text = data.get('markdown', '')
|
||||||
|
applied_styles = data.get('styles', {})
|
||||||
|
|
||||||
|
doc = convert_md_to_odt(markdown_text, applied_styles)
|
||||||
|
|
||||||
|
file_stream = io.BytesIO()
|
||||||
|
doc.save(file_stream, pretty=True)
|
||||||
|
file_stream.seek(0)
|
||||||
|
|
||||||
|
return send_file(
|
||||||
|
file_stream,
|
||||||
|
as_attachment=True,
|
||||||
|
download_name='document.odt',
|
||||||
|
mimetype='application/vnd.oasis.opendocument.text'
|
||||||
|
)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app.run(debug=True, host='0.0.0.0', port=5000)
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
Flask==2.3.3
|
||||||
|
gunicorn==23.0.0
|
||||||
|
markdown==3.4.4
|
||||||
|
odfdo==3.17.6
|
||||||
|
requests
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const markdownInput = document.getElementById('markdown-input');
|
||||||
|
const previewOutput = document.getElementById('preview-output');
|
||||||
|
const stylesList = document.getElementById('styles-list');
|
||||||
|
const exportButton = document.getElementById('export-odt');
|
||||||
|
const addTocButton = document.getElementById('add-toc');
|
||||||
|
const addStyleButton = document.getElementById('add-style');
|
||||||
|
const styleEditorModal = document.getElementById('style-editor-modal');
|
||||||
|
const styleEditorForm = document.getElementById('style-editor-form');
|
||||||
|
const styleModalTitle = document.getElementById('style-modal-title');
|
||||||
|
const styleNameInput = document.getElementById('style-name-input');
|
||||||
|
const fontFamilyInput = document.getElementById('font-family-input');
|
||||||
|
const fontSizeInput = document.getElementById('font-size-input');
|
||||||
|
const fontWeightInput = document.getElementById('font-weight-input');
|
||||||
|
const fontStyleInput = document.getElementById('font-style-input');
|
||||||
|
const textAlignInput = document.getElementById('text-align-input');
|
||||||
|
const colorInput = document.getElementById('color-input');
|
||||||
|
const backgroundColorInput = document.getElementById('background-color-input');
|
||||||
|
const paddingInput = document.getElementById('padding-input');
|
||||||
|
const borderLeftInput = document.getElementById('border-left-input');
|
||||||
|
const paddingLeftInput = document.getElementById('padding-left-input');
|
||||||
|
const marginBottomInput = document.getElementById('margin-bottom-input');
|
||||||
|
const closeButton = document.querySelector('.close-button');
|
||||||
|
const boldButton = document.getElementById('bold-button');
|
||||||
|
const italicButton = document.getElementById('italic-button');
|
||||||
|
const h1Button = document.getElementById('h1-button');
|
||||||
|
const h2Button = document.getElementById('h2-button');
|
||||||
|
|
||||||
|
let styles = {};
|
||||||
|
let appliedStyles = {};
|
||||||
|
|
||||||
|
async function fetchStyles() {
|
||||||
|
const response = await fetch('/styles');
|
||||||
|
styles = await response.json();
|
||||||
|
renderStyles();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderStyles() {
|
||||||
|
stylesList.innerHTML = '';
|
||||||
|
for (const name in styles) {
|
||||||
|
const styleItem = document.createElement('div');
|
||||||
|
styleItem.className = 'style-item';
|
||||||
|
styleItem.textContent = name;
|
||||||
|
styleItem.addEventListener('click', () => applyStyleToSelection(name));
|
||||||
|
stylesList.appendChild(styleItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyStyleToSelection(styleName) {
|
||||||
|
const start = markdownInput.selectionStart;
|
||||||
|
const end = markdownInput.selectionEnd;
|
||||||
|
const selectedText = markdownInput.value.substring(start, end);
|
||||||
|
const lines = markdownInput.value.substring(0, start).split('\n');
|
||||||
|
const startLine = lines.length - 1;
|
||||||
|
|
||||||
|
for (let i = 0; i < selectedText.split('\n').length; i++) {
|
||||||
|
appliedStyles[startLine + i] = styleName;
|
||||||
|
}
|
||||||
|
updatePreview();
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyMarkdownFormatting(prefix, suffix = prefix) {
|
||||||
|
const start = markdownInput.selectionStart;
|
||||||
|
const end = markdownInput.selectionEnd;
|
||||||
|
const selectedText = markdownInput.value.substring(start, end);
|
||||||
|
const newText = prefix + selectedText + suffix;
|
||||||
|
markdownInput.value = markdownInput.value.substring(0, start) + newText + markdownInput.value.substring(end);
|
||||||
|
updatePreview();
|
||||||
|
}
|
||||||
|
|
||||||
|
boldButton.addEventListener('click', () => applyMarkdownFormatting('**'));
|
||||||
|
italicButton.addEventListener('click', () => applyMarkdownFormatting('*'));
|
||||||
|
h1Button.addEventListener('click', () => applyMarkdownFormatting('# '));
|
||||||
|
h2Button.addEventListener('click', () => applyMarkdownFormatting('## '));
|
||||||
|
|
||||||
|
async function updatePreview() {
|
||||||
|
const markdown = markdownInput.value;
|
||||||
|
const response = await fetch('/preview', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ markdown, styles: appliedStyles }),
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
previewOutput.innerHTML = data.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
markdownInput.addEventListener('input', updatePreview);
|
||||||
|
|
||||||
|
exportButton.addEventListener('click', async () => {
|
||||||
|
const markdown = markdownInput.value;
|
||||||
|
const response = await fetch('/export', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ markdown, styles: appliedStyles }),
|
||||||
|
});
|
||||||
|
const blob = await response.blob();
|
||||||
|
const url = window.URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = 'document.odt';
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
a.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
addTocButton.addEventListener('click', () => {
|
||||||
|
const cursorPos = markdownInput.selectionStart;
|
||||||
|
const textBefore = markdownInput.value.substring(0, cursorPos);
|
||||||
|
const textAfter = markdownInput.value.substring(cursorPos);
|
||||||
|
markdownInput.value = textBefore + '[TOC]\n' + textAfter;
|
||||||
|
updatePreview();
|
||||||
|
});
|
||||||
|
|
||||||
|
addStyleButton.addEventListener('click', () => {
|
||||||
|
styleModalTitle.textContent = 'Add New Style';
|
||||||
|
styleEditorForm.reset();
|
||||||
|
styleNameInput.value = '';
|
||||||
|
styleEditorModal.style.display = 'block';
|
||||||
|
});
|
||||||
|
|
||||||
|
closeButton.addEventListener('click', () => {
|
||||||
|
styleEditorModal.style.display = 'none';
|
||||||
|
});
|
||||||
|
|
||||||
|
styleEditorForm.addEventListener('submit', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const name = styleNameInput.value;
|
||||||
|
const style = {
|
||||||
|
'font-family': fontFamilyInput.value,
|
||||||
|
'font-size': fontSizeInput.value,
|
||||||
|
'font-weight': fontWeightInput.value,
|
||||||
|
'font-style': fontStyleInput.value,
|
||||||
|
'text-align': textAlignInput.value,
|
||||||
|
'color': colorInput.value,
|
||||||
|
'background-color': backgroundColorInput.value,
|
||||||
|
'padding': paddingInput.value,
|
||||||
|
'border-left': borderLeftInput.value,
|
||||||
|
'padding-left': paddingLeftInput.value,
|
||||||
|
'margin-bottom': marginBottomInput.value,
|
||||||
|
};
|
||||||
|
await fetch(`/styles/${name}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(style),
|
||||||
|
});
|
||||||
|
styleEditorModal.style.display = 'none';
|
||||||
|
fetchStyles();
|
||||||
|
});
|
||||||
|
|
||||||
|
fetchStyles();
|
||||||
|
});
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
body {
|
||||||
|
font-family: 'Arial', sans-serif;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
color: #202124;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-container {
|
||||||
|
display: flex;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-pane {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
border-right: 1px solid #dadce0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar {
|
||||||
|
padding: 8px 16px;
|
||||||
|
background-color: #edf2fa;
|
||||||
|
border-bottom: 1px solid #dadce0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar button {
|
||||||
|
background-color: transparent;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 18px;
|
||||||
|
margin-right: 16px;
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar button:hover {
|
||||||
|
background-color: #d2e3fc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-input {
|
||||||
|
flex: 1;
|
||||||
|
padding: 48px;
|
||||||
|
border: none;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.6;
|
||||||
|
resize: none;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-pane {
|
||||||
|
flex: 1;
|
||||||
|
padding: 48px;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-output {
|
||||||
|
max-width: 8.5in;
|
||||||
|
margin: 0 auto;
|
||||||
|
background-color: #fff;
|
||||||
|
padding: 1in;
|
||||||
|
box-shadow: 0 0 10px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.styles-pane {
|
||||||
|
width: 250px;
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #edf2fa;
|
||||||
|
border-left: 1px solid #dadce0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#styles-list {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.style-item {
|
||||||
|
padding: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
background-color: #fff;
|
||||||
|
border: 1px solid #dadce0;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.style-item:hover {
|
||||||
|
background-color: #d2e3fc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
z-index: 1;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
background-color: rgba(0,0,0,0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
background-color: #fefefe;
|
||||||
|
margin: 15% auto;
|
||||||
|
padding: 20px;
|
||||||
|
border: 1px solid #888;
|
||||||
|
width: 80%;
|
||||||
|
max-width: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-button {
|
||||||
|
color: #aaa;
|
||||||
|
float: right;
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-button:hover,
|
||||||
|
.close-button:focus {
|
||||||
|
color: black;
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
+54
@@ -0,0 +1,54 @@
|
|||||||
|
{
|
||||||
|
"Normal": {
|
||||||
|
"font_size": "12pt",
|
||||||
|
"font_family": "Liberation Serif",
|
||||||
|
"margin_bottom": "0.1in"
|
||||||
|
},
|
||||||
|
"Heading 1": {
|
||||||
|
"font_size": "18pt",
|
||||||
|
"font_family": "Liberation Sans",
|
||||||
|
"font_weight": "bold",
|
||||||
|
"margin_bottom": "0.2in"
|
||||||
|
},
|
||||||
|
"Heading 2": {
|
||||||
|
"font_size": "14pt",
|
||||||
|
"font_family": "Liberation Sans",
|
||||||
|
"font_weight": "bold",
|
||||||
|
"margin_bottom": "0.15in"
|
||||||
|
},
|
||||||
|
"Caption": {
|
||||||
|
"font_size": "10pt",
|
||||||
|
"font_family": "Liberation Serif",
|
||||||
|
"font_style": "italic",
|
||||||
|
"text_align": "center"
|
||||||
|
},
|
||||||
|
"Code": {
|
||||||
|
"font_family": "Courier New",
|
||||||
|
"font_size": "10pt",
|
||||||
|
"background_color": "#f0f0f0",
|
||||||
|
"padding": "10px"
|
||||||
|
},
|
||||||
|
"Quote": {
|
||||||
|
"font_style": "italic",
|
||||||
|
"border_left": "3px solid #ccc",
|
||||||
|
"padding_left": "10px"
|
||||||
|
},
|
||||||
|
"Test Style": {
|
||||||
|
"font-family": "Comic Sans MS",
|
||||||
|
"font-size": "20pt",
|
||||||
|
"color": "#ff00ff"
|
||||||
|
},
|
||||||
|
"My New Style": {
|
||||||
|
"font-family": "Georgia",
|
||||||
|
"font-size": "14pt",
|
||||||
|
"font-weight": "",
|
||||||
|
"font-style": "",
|
||||||
|
"text-align": "",
|
||||||
|
"color": "blue",
|
||||||
|
"background-color": "",
|
||||||
|
"padding": "",
|
||||||
|
"border-left": "",
|
||||||
|
"padding-left": "",
|
||||||
|
"margin-bottom": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>GhostEditor</title>
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="main-container">
|
||||||
|
<div class="editor-pane">
|
||||||
|
<div class="toolbar">
|
||||||
|
<button id="bold-button" title="Bold"><i class="fas fa-bold"></i></button>
|
||||||
|
<button id="italic-button" title="Italic"><i class="fas fa-italic"></i></button>
|
||||||
|
<button id="h1-button" title="Heading 1">H1</button>
|
||||||
|
<button id="h2-button" title="Heading 2">H2</button>
|
||||||
|
<button id="add-toc" title="Insert TOC"><i class="fas fa-list-ul"></i></button>
|
||||||
|
<button id="export-odt" title="Export as ODT"><i class="fas fa-file-export"></i></button>
|
||||||
|
</div>
|
||||||
|
<textarea id="markdown-input" class="markdown-input"></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="preview-pane">
|
||||||
|
<div id="preview-output" class="preview-output"></div>
|
||||||
|
</div>
|
||||||
|
<div class="styles-pane">
|
||||||
|
<h2>Styles</h2>
|
||||||
|
<div id="styles-list"></div>
|
||||||
|
<button id="add-style">Add New Style</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="style-editor-modal" class="modal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<span class="close-button">×</span>
|
||||||
|
<h2 id="style-modal-title">Edit Style</h2>
|
||||||
|
<form id="style-editor-form">
|
||||||
|
<label for="style-name-input">Style Name:</label>
|
||||||
|
<input type="text" id="style-name-input">
|
||||||
|
<label for="font-family-input">Font Family:</label>
|
||||||
|
<input type="text" id="font-family-input">
|
||||||
|
<label for="font-size-input">Font Size:</label>
|
||||||
|
<input type="text" id="font-size-input">
|
||||||
|
<label for="font-weight-input">Font Weight:</label>
|
||||||
|
<input type="text" id="font-weight-input">
|
||||||
|
<label for="font-style-input">Font Style:</label>
|
||||||
|
<input type="text" id="font-style-input">
|
||||||
|
<label for="text-align-input">Text Align:</label>
|
||||||
|
<input type="text" id="text-align-input">
|
||||||
|
<label for="color-input">Color:</label>
|
||||||
|
<input type="text" id="color-input">
|
||||||
|
<label for="background-color-input">Background Color:</label>
|
||||||
|
<input type="text" id="background-color-input">
|
||||||
|
<label for="padding-input">Padding:</label>
|
||||||
|
<input type="text" id="padding-input">
|
||||||
|
<label for="border-left-input">Border Left:</label>
|
||||||
|
<input type="text" id="border-left-input">
|
||||||
|
<label for="padding-left-input">Padding Left:</label>
|
||||||
|
<input type="text" id="padding-left-input">
|
||||||
|
<label for="margin-bottom-input">Margin Bottom:</label>
|
||||||
|
<input type="text" id="margin-bottom-input">
|
||||||
|
<button type="submit">Save Style</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="{{ url_for('static', filename='script.js') }}"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user