diff --git a/app.py b/app.py index 26e2ade..496c2a2 100644 --- a/app.py +++ b/app.py @@ -1,223 +1,324 @@ 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 +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 -from werkzeug.utils import secure_filename +import requests +import os +import tempfile +from urllib.parse import urlparse app = Flask(__name__) -# Global variables for tracking captions and table pagination -image_counter = 0 -table_counter = 0 +ALLOWED_IMAGE_DOMAINS = ["upload.wikimedia.org"] -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 +# 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) -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 +@app.route('/styles', methods=['GET']) +def get_styles(): + return jsonify(styles) + +@app.route('/styles/', 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 - - # Create a new document - doc = Document() - - # Add custom styles - add_custom_styles(doc) - - # Parse markdown into lines - lines = markdown_text.split('\n') - + 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].strip() - + 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('# '): - # Heading 1 - doc.add_heading(line[2:], level=0) + 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('## '): - # 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 + 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 - # 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 = [] + 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(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 - + 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 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('!['): - # Image with potential caption - # Extract image info: ![alt text](url) - 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) + 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"
{line}
" + + full_html = f"{styled_html}" + + return jsonify({'html': full_html}) + @app.route('/export', methods=['POST']) def export(): - markdown_text = request.form.get('markdown', '') - config_json = request.form.get('config', '{}') + data = request.json + markdown_text = data.get('markdown', '') + applied_styles = data.get('styles', {}) - try: - config = json.loads(config_json) - except: - config = {} + doc = convert_md_to_odt(markdown_text, applied_styles) - # Convert markdown to docx - doc = convert_md_to_docx(markdown_text, config) - - # Save to BytesIO object file_stream = io.BytesIO() - doc.save(file_stream) + doc.save(file_stream, pretty=True) 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' + download_name='document.odt', + mimetype='application/vnd.oasis.opendocument.text' ) if __name__ == '__main__': - app.run(debug=True, host='0.0.0.0', port=5000) \ No newline at end of file + app.run(debug=True, host='0.0.0.0', port=5000) diff --git a/requirements.txt b/requirements.txt index a9270b7..1fbd8a9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,5 @@ Flask==2.3.3 -python-docx==0.8.11 -markdown==3.4.4 \ No newline at end of file +gunicorn==23.0.0 +markdown==3.4.4 +odfdo==3.17.6 +requests diff --git a/static/script.js b/static/script.js index 6e1e475..5b12307 100644 --- a/static/script.js +++ b/static/script.js @@ -1,298 +1,157 @@ -// 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 +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'); -This is a sample document to demonstrate the real-time conversion capabilities of GhostEditor. + let styles = {}; + let appliedStyles = {}; -## Features Demonstrated - -### Auto-captioning -![Sample Image](https://via.placeholder.com/150) -*Figure 1: Sample placeholder image* - -### Table with Pagination Support -| ID | Name | Role | -|----|------|------| -| 1 | John | Developer | -| 2 | Jane | Designer | -| 3 | Bob | Manager | -| 4 | Alice | Analyst | - -### Code Block -\`\`\`python -def hello_world(): - print("Hello, GhostEditor!") -\`\`\` - -### Blockquote -> This is a sample quote demonstrating the styling capabilities of GhostEditor. - -### List -- First item -- Second item -- Third item`; - - // Update configuration when settings change - autoCaptionImages.addEventListener('change', updateConfig); - autoCaptionTables.addEventListener('change', updateConfig); - autoCaptionFigures.addEventListener('change', updateConfig); - tablePagination.addEventListener('change', updateConfig); - - // Toolbar buttons - boldBtn.addEventListener('click', () => wrapText('**', '**')); - italicBtn.addEventListener('click', () => wrapText('*', '*')); - headingBtn.addEventListener('click', () => wrapLine('# ')); - listBtn.addEventListener('click', () => wrapLine('- ')); - imageBtn.addEventListener('click', () => insertImage()); - - // Apply style button - applyStyleBtn.addEventListener('click', applyStyleToSelection); - - // Apply caption button - applyCaptionBtn.addEventListener('click', applyCaptionToSelection); - - // Export button - exportBtn.addEventListener('click', exportToWord); - - // Initial preview update - updatePreview(); - - // Update preview when input changes - markdownInput.addEventListener('input', updatePreview); - - // Update preview when toggle changes - previewToggle.addEventListener('change', function() { - if (this.checked) { - updatePreview(); - } else { - previewOutput.innerHTML = '

Preview is disabled. Enable to see real-time conversion.

'; - } - }); - - // Update configuration - function updateConfig() { - config = { - autoCaptionImages: autoCaptionImages.checked, - autoCaptionTables: autoCaptionTables.checked, - autoCaptionFigures: autoCaptionFigures.checked, - tablePagination: tablePagination.checked, - defaultStyle: document.getElementById('defaultStyle').value - }; - - // Update preview after config change - updatePreview(); + async function fetchStyles() { + const response = await fetch('/styles'); + styles = await response.json(); + renderStyles(); } - - // 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(); + + 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); } } - - // 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(//g, '
'); - } - - 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 = '

Heading 1 Example

'; - break; - case 'Heading2': - styleExample = '

Heading 2 Example

'; - break; - case 'Heading3': - styleExample = '

Heading 3 Example

'; - break; - case 'Quote': - styleExample = '
This is a quote example
'; - break; - case 'Code': - styleExample = 'This is a code example'; - break; - default: - styleExample = '

This is a normal paragraph example

'; - } - - 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) { + + 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; - const newText = before + selectedText + after; + 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); - - // 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; - + + 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(); - } - - // 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 = `![${altText}](path/to/image.png)`; - - 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); -}); \ No newline at end of file + }); + + 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(); +}); diff --git a/static/style.css b/static/style.css index 168eff7..3c8bb0d 100644 --- a/static/style.css +++ b/static/style.css @@ -1,165 +1,123 @@ -/* GhostEditor - Markdown to Word Converter Styles */ body { + font-family: 'Arial', sans-serif; + margin: 0; + padding: 0; background-color: #f8f9fa; - font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + color: #202124; } -.card { - border: 1px solid #dee2e6; - border-radius: 0.5rem; - box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075); +.main-container { + display: flex; + height: 100vh; } -.card-header { - background-color: #e9ecef; - border-bottom: 1px solid #dee2e6; - font-weight: 600; +.editor-pane { + flex: 1; + display: flex; + flex-direction: column; + border-right: 1px solid #dadce0; } -#markdownInput { +.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; - height: 500px; - font-family: 'Courier New', monospace; + background-color: #fff; } -.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 { +.preview-pane { + flex: 1; + padding: 48px; background-color: #f8f9fa; - border: 1px solid #e9ecef; - border-radius: 0.375rem; - padding: 1rem; - overflow-x: auto; + overflow-y: auto; } -#previewOutput code { - background-color: #f1f2f6; - padding: 0.2rem 0.4rem; - border-radius: 0.25rem; - font-family: 'Courier New', monospace; +.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); } -#previewOutput blockquote { - border-left: 4px solid #3498db; - margin: 1rem 0; - padding-left: 1rem; - color: #7f8c8d; - font-style: italic; +.styles-pane { + width: 250px; + padding: 20px; + background-color: #edf2fa; + border-left: 1px solid #dadce0; } -#previewOutput img { - max-width: 100%; - height: auto; - margin: 1rem 0; +#styles-list { + margin-top: 20px; } -/* Style preview */ -#stylePreview { - min-height: 100px; - border: 1px dashed #ced4da; - padding: 0.5rem; - border-radius: 0.25rem; +.style-item { + padding: 10px; + margin-bottom: 10px; + background-color: #fff; + border: 1px solid #dadce0; + cursor: pointer; } -/* Responsive adjustments */ -@media (max-width: 768px) { - .container-fluid { - padding: 0.5rem; - } - - .row > div { - margin-bottom: 1rem; - } +.style-item:hover { + background-color: #d2e3fc; } -/* Table pagination styles */ -.table-pagination-header { - background-color: #ecf0f1; +.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; - 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; +.close-button:hover, +.close-button:focus { + color: black; + text-decoration: none; + cursor: pointer; } - -/* 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; -} \ No newline at end of file diff --git a/styles.json b/styles.json new file mode 100644 index 0000000..60e9df8 --- /dev/null +++ b/styles.json @@ -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": "" + } +} \ No newline at end of file diff --git a/templates/index.html b/templates/index.html index c3d97dc..7916728 100644 --- a/templates/index.html +++ b/templates/index.html @@ -3,173 +3,67 @@ - GhostEditor - Markdown to Word Converter - - - + GhostEditor + + -
-
-

GhostEditor

-

Real-time Markdown to Word converter with advanced styling and auto-captioning

-
- -
- -
-
-
-
Document Settings
-
-
-
- - -
- -
- - -
- -
- -
- - -
-
- - -
-
- - -
-
- -
- -
- - -
-
- - -
-
- -
-
-
Style Preview
-
-
-
-

Select text to apply styles

-
-
-
-
- - -
-
-
-
-
-
Markdown Editor
-
- - - - - -
-
-
- -
-
-
- -
-
-
-
Real-time Preview
-
- - -
-
-
-
-
-
-
-
- - -
-
-
Block Styling
-
-
-
-
- - -
-
- - -
-
- - -
-
-
- - -
-
-
+
+
+
+ + + + + +
+ +
+
+
+
+
+

Styles

+
+
- - - + + + - \ No newline at end of file +