2 Commits

Author SHA1 Message Date
google-labs-jules[bot] 144bfb2ba7 feat: Implement Google Docs-style editor with ODT export
This commit introduces a complete overhaul of the application, replacing the
previous simple Markdown editor with a powerful, Google Docs-style
editor with a live preview and ODT export.

The new features include:
- A new three-pane UI with a Markdown editor, a live preview, and a
  styles panel.
- A powerful styling engine that allows users to create, edit, and
  apply custom styles to the document.
- A persistent storage mechanism for styles using a JSON file.
- Support for advanced Markdown features, including code blocks and
  blockquotes.
- A hyperlinked table of contents that is automatically generated from
  the headings in the document.
- Auto-captioning for tables and images.
- Pagination for large tables.
- Secure image embedding with a domain whitelist and size limits to
  prevent SSRF and DoS attacks.
- An improved live preview that correctly renders multi-line Markdown
  elements.
2025-12-05 12:17:59 +00:00
qwen.ai[bot] 368426f0f4 **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
2025-12-05 01:13:54 +00:00
9 changed files with 956 additions and 202 deletions
+48 -3
View File
@@ -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/
```
+93
View File
@@ -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
+73 -189
View File
@@ -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
```
**Плюсы**:
- Интуитивный API, близкий к логике Word.
- Поддержка всех ключевых частей пакета (headers, footers, styles).
**Минусы**:
- Нет поддержки макросов.
- Ограниченная работа с графикой (изображения добавляются, но редактирование сложно).
**Ссылка**: [python-docx.readthedocs.io](https://python-docx.readthedocs.io/)
---
#### **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")
2. Run the application:
```bash
python app.py
```
**Плюсы**:
- Идеален для шаблонизированных отчетов (договоры, счета).
- Интеграция с Django/Flask.
3. Open your browser and navigate to `http://localhost:5000`
**Минусы**:
- Требует предварительной настройки шаблонов в Word.
## Usage
**Ссылка**: [github.com/elapouya/python-docx-template](https://github.com/elapouya/python-docx-template)
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
#### **3. mammoth**
**Назначение**: Конвертация .docx → HTML с сохранением структуры.
**Особенности**:
- Преобразует стили Word в CSS-классы.
- Подходит для извлечения контента из сложных документов.
### Real-time Preview
- The application converts Markdown to Word format as you type
- Preview updates automatically as you make changes
**Пример**:
```python
import mammoth
### Auto Captioning
- Images automatically receive "Figure X" captions
- Tables automatically receive "Table X" captions
- Captions are numbered sequentially
with open("document.docx", "rb") as docx_file:
result = mammoth.convert_to_html(docx_file)
html = result.value # HTML-строка
warnings = result.messages # Предупреждения
### 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
```
**Плюсы**:
- Сохраняет семантику (заголовки, списки, таблицы).
- Легко интегрируется с парсерами (BeautifulSoup).
## Technologies Used
**Минусы**:
- Не подходит для обратной конвертации (HTML → docx).
- **Frontend**: HTML, CSS, JavaScript, Bootstrap 5, Marked.js
- **Backend**: Flask (Python)
- **Document Processing**: python-docx
- **Markdown Processing**: markdown (Python library)
**Ссылка**: [github.com/mwilliamson/mammoth.py](https://github.com/mwilliamson/mammoth.py)
## Customization
---
### **Низкоуровневые инструменты (для кастомных задач)**
#### **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
+324
View File
@@ -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)
+5
View File
@@ -0,0 +1,5 @@
Flask==2.3.3
gunicorn==23.0.0
markdown==3.4.4
odfdo==3.17.6
requests
+157
View File
@@ -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();
});
+123
View File
@@ -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
View File
@@ -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": ""
}
}
+69
View File
@@ -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">&times;</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>