Skip to content

Instantly share code, notes, and snippets.

@mshafiee
Last active January 15, 2025 07:42
Show Gist options
  • Save mshafiee/f4514dc751ea2ea9eef0bf94fda84e10 to your computer and use it in GitHub Desktop.
Save mshafiee/f4514dc751ea2ea9eef0bf94fda84e10 to your computer and use it in GitHub Desktop.
/*
TinyMCE Local Editor Server
This program 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:
go run main.go [-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 program 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 to the `/save` endpoint.
Requirements:
- Go installed on your system.
- TinyMCE library files placed in the `js/tinymce` directory relative to the program.
How to Download and Save TinyMCE for This Program:
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 Go program 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/
├── main.go
├── js/
│ └── tinymce/
│ ├── tinymce.min.js
│ ├── skins/
│ ├── themes/
│ └── plugins/
└── your_html_file.html
6. Run the Program:
- Now, when you run the program, it will load TinyMCE from the `js/tinymce` directory.
Examples:
1. Edit an HTML file with default LTR text direction:
go run main.go ./example.html
2. Edit an HTML file with RTL text direction:
go run main.go -dir rtl ./example.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.
Endpoints:
- GET /: Serves the HTML file with the embedded TinyMCE editor.
- POST /save: Saves the content from the TinyMCE editor back to the original HTML file.
- GET /js/tinymce/: Serves static TinyMCE files (e.g., JavaScript, CSS, images).
Notes:
- The server automatically finds a free port to avoid conflicts.
- The editor supports auto-save functionality, which can be toggled on/off.
- The program opens the editor in your default web browser automatically.
- The `-dir` flag allows you to specify the text direction for the editor, making it suitable for both LTR and RTL languages.
*/
package main
import (
"flag"
"fmt"
"io/ioutil"
"log"
"net"
"net/http"
"os"
"os/exec"
"runtime"
)
const (
TINYMCE_PATH = "/js/tinymce"
)
var (
htmlFile string
dirFlag string
)
func main() {
flag.StringVar(&dirFlag, "dir", "ltr", "Text direction: 'ltr' or 'rtl'")
flag.Parse()
if len(flag.Args()) != 1 {
log.Fatal("Usage: go run main.go [-dir ltr|rtl] <html_file>")
}
htmlFile = flag.Arg(0)
if _, err := os.Stat(htmlFile); os.IsNotExist(err) {
log.Fatalf("Error: File '%s' not found.", htmlFile)
}
http.HandleFunc("/", serveEditor)
http.HandleFunc("/save", saveContent)
http.Handle(TINYMCE_PATH+"/", http.StripPrefix(TINYMCE_PATH, http.FileServer(http.Dir("js/tinymce"))))
port := findFreePort()
log.Printf("Serving at http://localhost:%d\n", port)
openBrowser(fmt.Sprintf("http://localhost:%d", port))
log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", port), nil))
}
func serveEditor(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
serveStaticFile(w, r)
return
}
content, err := ioutil.ReadFile(htmlFile)
if err != nil {
http.Error(w, fmt.Sprintf("Error reading file: %s", err), http.StatusInternalServerError)
return
}
modifiedContent := injectTinyMCE(string(content), dirFlag)
w.Header().Set("Content-Type", "text/html")
w.Write([]byte(modifiedContent))
}
func serveStaticFile(w http.ResponseWriter, r *http.Request) {
filePath := "." + r.URL.Path
http.ServeFile(w, r, filePath)
}
func saveContent(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Invalid request method", http.StatusMethodNotAllowed)
return
}
body, err := ioutil.ReadAll(r.Body)
if err != nil {
http.Error(w, fmt.Sprintf("Error reading request body: %s", err), http.StatusInternalServerError)
return
}
err = ioutil.WriteFile(htmlFile, body, 0644)
if err != nil {
http.Error(w, fmt.Sprintf("Error writing file: %s", err), http.StatusInternalServerError)
return
}
w.Write([]byte("Saved"))
}
func injectTinyMCE(content, dir string) string {
template := `<!DOCTYPE html>
<html lang="en" dir="%s">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TinyMCE Editor</title>
<script src="%s/tinymce.min.js"></script>
<style>
html, body {
margin: 0;
padding: 0;
height: 100%%;
overflow: hidden;
direction: %s;
}
#editor {
height: 100vh;
width: 100%%;
}
</style>
<script>
document.addEventListener('DOMContentLoaded', function () {
let autoSaveEnabled = false;
tinymce.init({
selector: '#editor',
height: '100%%',
width: '100%%',
directionality: '%s',
plugins: [
'advlist autolink lists link image charmap print preview anchor',
'searchreplace visualblocks code fullscreen',
'insertdatetime media table paste code help wordcount',
'save autosave'
],
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) {
editor.ui.registry.addButton('save', {
text: 'Save',
icon: 'save',
onAction: function () {
saveContent(editor);
}
});
editor.ui.registry.addToggleButton('autosave', {
text: 'Auto-Save',
icon: 'autosave',
onAction: function (api) {
autoSaveEnabled = !autoSaveEnabled;
api.setActive(autoSaveEnabled);
console.log('Auto-save is now', autoSaveEnabled ? 'enabled' : 'disabled');
},
onSetup: function (api) {
api.setActive(autoSaveEnabled);
}
});
editor.addCommand('saveContent', function () {
saveContent(editor);
});
editor.ui.registry.addMenuItem('save', {
text: 'Save',
icon: 'save',
onAction: function () {
saveContent(editor);
}
});
editor.addShortcut('Meta+S', 'Save', function () {
saveContent(editor);
});
editor.addShortcut('Ctrl+S', 'Save', function () {
saveContent(editor);
});
editor.on('change', function () {
if (autoSaveEnabled) {
saveContent(editor);
}
});
}
});
function saveContent(editor) {
fetch('/save', {
method: 'POST',
body: editor.getContent(),
headers: { 'Content-Type': 'text/plain' }
})
.then(response => response.text())
.then(message => {
console.log(message);
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">%s</textarea>
</body>
</html>`
return fmt.Sprintf(template, dir, TINYMCE_PATH, dir, dir, content)
}
func findFreePort() int {
listener, err := net.Listen("tcp", ":0")
if err != nil {
log.Fatal(err)
}
defer listener.Close()
return listener.Addr().(*net.TCPAddr).Port
}
func openBrowser(url string) {
var err error
switch runtime.GOOS {
case "linux":
err = exec.Command("xdg-open", url).Start()
case "windows":
err = exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start()
case "darwin":
err = exec.Command("open", url).Start()
default:
err = fmt.Errorf("unsupported platform")
}
if err != nil {
log.Fatal(err)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment