Skip to content

Instantly share code, notes, and snippets.

@mshafiee
Last active January 15, 2025 07:44
Show Gist options
  • Save mshafiee/06ae7b8bdc506d292a48b9d0e92d6b5d to your computer and use it in GitHub Desktop.
Save mshafiee/06ae7b8bdc506d292a48b9d0e92d6b5d to your computer and use it in GitHub Desktop.
"""
TinyMCE Local Editor Server
This script is a simple HTTP server that serves a local HTML file with an embedded TinyMCE editor.
It allows you to edit the content of the HTML file in real-time using the TinyMCE rich text editor.
The server also saves any changes made in the editor back to the original HTML file.
Usage:
python tinymce_editor.py [-dir ltr|rtl] <html_file>
Arguments:
<html_file> - The path to the HTML file you want to edit.
Flags:
-dir - Specifies the text direction for the editor. Valid values are "ltr" (Left-to-Right) or "rtl" (Right-to-Left).
Default: "ltr".
How it works:
1. The script starts a local HTTP server on a free port.
2. It opens the specified HTML file and injects the TinyMCE editor into it.
3. The editor is configured to work in either LTR or RTL mode, depending on the `-dir` flag.
4. Any changes made in the editor are automatically saved back to the original HTML file via a POST request.
Requirements:
- Python 3.x
- TinyMCE library (place the TinyMCE files in a directory named 'js/tinymce' relative to the script)
How to Download and Save TinyMCE for This Script:
1. Download TinyMCE:
- Visit the TinyMCE download page: https://www.tiny.cloud/get-tiny/self-hosted/
- Choose the version you want to download (e.g., the latest stable version).
- Download the .zip file for the self-hosted version.
2. Extract the Files:
- Extract the contents of the downloaded .zip file to a temporary folder.
3. Create the Required Directory:
- In the same directory where your Python script is located, create a folder named 'js'.
- Inside the 'js' folder, create another folder named 'tinymce'.
4. Copy TinyMCE Files:
- From the extracted TinyMCE folder, copy the contents of the 'tinymce' directory (e.g., tinymce.min.js, skins, themes, plugins, etc.) into the 'js/tinymce' folder you created in the previous step.
5. Verify the Structure:
- Ensure the directory structure looks like this:
your_project_directory/
├── tinymce_editor.py
├── js/
│ └── tinymce/
│ ├── tinymce.min.js
│ ├── skins/
│ ├── themes/
│ └── plugins/
└── your_html_file.html
6. Run the Script:
- Now, when you run the script, it will load TinyMCE from the 'js/tinymce' directory.
Examples:
1. Edit an HTML file with default LTR text direction:
python tinymce_editor.py myfile.html
2. Edit an HTML file with RTL text direction:
python tinymce_editor.py -dir rtl myfile.html
This will start the server and open the editor in your default web browser. You can then edit the content of the HTML file and save it directly from the editor.
"""
import os
import sys
import socket
import logging
import argparse
from http.server import BaseHTTPRequestHandler, HTTPServer
import webbrowser
from urllib.parse import urlparse, parse_qs
# Configure logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
# TinyMCE local path
TINYMCE_PATH = "/js/tinymce"
class TinyMCEHandler(BaseHTTPRequestHandler):
def __init__(self, *args, **kwargs):
self.html_file = kwargs.pop('html_file')
self.text_direction = kwargs.pop('text_direction')
super().__init__(*args, **kwargs)
def do_GET(self):
if self.path == "/":
self.serve_editor()
elif self.path.startswith(TINYMCE_PATH):
self.serve_static_file("." + self.path)
else:
self.serve_static_file("." + self.path)
def do_POST(self):
if self.path.startswith("/save"):
self.save_content()
def serve_editor(self):
try:
with open(self.html_file, "r") as f:
content = f.read()
modified_content = self.inject_tinymce(content, self.text_direction)
self.send_response(200)
self.send_header("Content-type", "text/html")
self.end_headers()
self.wfile.write(modified_content.encode("utf-8"))
except Exception as e:
logging.error(f"Error serving editor: {e}")
self.send_error(500, f"Internal Server Error: {str(e)}")
def serve_static_file(self, file_path):
try:
if os.path.isfile(file_path):
with open(file_path, "rb") as f:
self.send_response(200)
self.send_header("Content-type", self.get_mime_type(file_path))
self.end_headers()
self.wfile.write(f.read())
else:
self.send_error(404, "File Not Found")
except Exception as e:
logging.error(f"Error serving static file: {e}")
self.send_error(500, f"Internal Server Error: {str(e)}")
def save_content(self):
try:
parsed_url = urlparse(self.path)
query_params = parse_qs(parsed_url.query)
filename = query_params.get('filename', [self.html_file])[0]
content_length = int(self.headers["Content-Length"])
post_data = self.rfile.read(content_length).decode("utf-8")
with open(filename, "w") as f:
f.write(post_data)
self.send_response(200)
self.end_headers()
self.wfile.write(b"Saved")
except Exception as e:
logging.error(f"Error saving content: {e}")
self.send_error(500, f"Internal Server Error: {str(e)}")
def inject_tinymce(self, content, text_direction):
return f"""
<!DOCTYPE html>
<html lang="en" dir="{text_direction}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TinyMCE Editor</title>
<script src="{TINYMCE_PATH}/tinymce.min.js"></script>
<style>
html, body {{
margin: 0;
padding: 0;
height: 100%;
overflow: hidden;
direction: {text_direction}; /* Set page direction */
}}
#editor {{
height: 100vh;
width: 100%;
}}
</style>
<script>
document.addEventListener('DOMContentLoaded', function () {{
let autoSaveEnabled = false; // Default: auto-save is inactive
tinymce.init({{
selector: '#editor',
height: '100%',
width: '100%',
directionality: '{text_direction}', /* Set TinyMCE editor direction */
plugins: [
'advlist autolink lists link image charmap print preview anchor',
'searchreplace visualblocks code fullscreen',
'insertdatetime media table paste code help wordcount',
'save autosave' // Add save and autosave plugins
],
toolbar: `
undo redo | formatselect | bold italic underline strikethrough |
alignleft aligncenter alignright alignjustify | outdent indent |
bullist numlist | link image media table | forecolor backcolor |
code | fullscreen | save | autosave
`,
menu: {{
file: {{ title: 'File', items: 'save restoredraft | preview | print | export' }},
edit: {{ title: 'Edit', items: 'undo redo | cut copy paste | selectall | searchreplace' }},
view: {{ title: 'View', items: 'code | visualaid visualchars visualblocks | fullscreen' }},
insert: {{ title: 'Insert', items: 'image link media template codesample inserttable | charmap emoticons hr | pagebreak nonbreaking anchor toc | insertdatetime' }},
format: {{ title: 'Format', items: 'bold italic underline strikethrough superscript subscript codeformat | formats blockformats fontformats fontsizes align | forecolor backcolor | removeformat' }},
tools: {{ title: 'Tools', items: 'code wordcount spellchecker | restoredraft' }},
table: {{ title: 'Table', items: 'inserttable tableprops deletetable | cell row column' }},
}},
setup: function (editor) {{
// Add a custom save button to the toolbar
editor.ui.registry.addButton('save', {{
text: 'Save',
icon: 'save',
onAction: function () {{
saveContent(editor);
}}
}});
// Add a custom auto-save toggle button to the toolbar
editor.ui.registry.addToggleButton('autosave', {{
text: 'Auto-Save',
icon: 'autosave',
onAction: function (api) {{
autoSaveEnabled = !autoSaveEnabled; // Toggle auto-save state
api.setActive(autoSaveEnabled); // Update button state
console.log('Auto-save is now', autoSaveEnabled ? 'enabled' : 'disabled');
}},
onSetup: function (api) {{
api.setActive(autoSaveEnabled); // Set initial button state
}}
}});
// Add a custom save command
editor.addCommand('saveContent', function () {{
saveContent(editor);
}});
// Add a custom save menu item to the File menu
editor.ui.registry.addMenuItem('save', {{
text: 'Save',
icon: 'save',
onAction: function () {{
saveContent(editor);
}}
}});
// Add keyboard shortcut for save (Ctrl+S or Command+S)
editor.addShortcut('Meta+S', 'Save', function () {{
saveContent(editor);
}});
editor.addShortcut('Ctrl+S', 'Save', function () {{
saveContent(editor);
}});
// Handle auto-save on change
editor.on('change', function () {{
if (autoSaveEnabled) {{
saveContent(editor);
}}
}});
}}
}});
// Function to save content
function saveContent(editor) {{
fetch('/save?filename={self.html_file}', {{
method: 'POST',
body: editor.getContent(),
headers: {{ 'Content-Type': 'text/plain' }}
}})
.then(response => response.text())
.then(message => {{
console.log(message); // Log the save confirmation
if (autoSaveEnabled) {{
console.log('Auto-save successful!');
}} else {{
alert('File saved successfully!');
}}
}})
.catch(error => {{
console.error('Error saving file:', error);
alert('Error saving file!');
}});
}}
}});
</script>
</head>
<body>
<textarea id="editor">{content}</textarea>
</body>
</html>
"""
def get_mime_type(self, file_path):
if file_path.endswith(".js"):
return "application/javascript"
elif file_path.endswith(".css"):
return "text/css"
elif file_path.endswith(".png"):
return "image/png"
elif file_path.endswith(".jpg") or file_path.endswith(".jpeg"):
return "image/jpeg"
elif file_path.endswith(".gif"):
return "image/gif"
elif file_path.endswith(".html"):
return "text/html"
elif file_path.endswith(".txt"):
return "text/plain"
else:
return "application/octet-stream"
def find_free_port():
"""Find a free port to use for the server."""
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind(('', 0)) # Bind to a free port provided by the OS
return s.getsockname()[1] # Return the port number
def run_server(html_file, text_direction):
port = find_free_port() # Use a random free port
server_address = ("", port)
handler = lambda *args, **kwargs: TinyMCEHandler(*args, html_file=html_file, text_direction=text_direction, **kwargs)
httpd = HTTPServer(server_address, handler)
logging.info(f"Serving at http://localhost:{port}")
webbrowser.open(f"http://localhost:{port}")
httpd.serve_forever()
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="TinyMCE Local Editor Server")
parser.add_argument("html_file", help="The path to the HTML file you want to edit.")
parser.add_argument("-dir", "--direction", choices=["ltr", "rtl"], default="ltr", help="Text direction: 'ltr' (Left-to-Right) or 'rtl' (Right-to-Left). Default: 'ltr'.")
args = parser.parse_args()
if not os.path.exists(args.html_file):
logging.error(f"Error: File '{args.html_file}' not found.")
sys.exit(1)
run_server(args.html_file, args.direction)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment