feat: replace Monaco by Ace
All checks were successful
/ pre-commit (push) Successful in 1m30s

- Remove the Monaco Editor because it was to heavy and hard to integrate
- Use Ace instead
- Use the lowercase identifier for languages (ex: "Text" -> "text")
- Select automatically the default language in the drop down to create a note
  (like the expiration)
- Add `ace_directory` to serve assets from a local folder instead of a CDN
- "hcl" syntax highlighting has been removed
- "go" syntax highlighting has been renamed to "golang"

Signed-off-by: Julien Riou <julien@riou.xyz>
This commit is contained in:
Julien Riou 2025-09-21 15:03:39 +02:00
commit 3147306927
Signed by: jriou
GPG key ID: 9A099EDA51316854
8 changed files with 92 additions and 71 deletions

6
package-lock.json generated
View file

@ -5,6 +5,7 @@
"packages": { "packages": {
"": { "": {
"dependencies": { "dependencies": {
"ace-builds": "^1.43.3",
"bootstrap": "^5.3.8" "bootstrap": "^5.3.8"
} }
}, },
@ -18,6 +19,11 @@
"url": "https://opencollective.com/popperjs" "url": "https://opencollective.com/popperjs"
} }
}, },
"node_modules/ace-builds": {
"version": "1.43.3",
"resolved": "https://registry.npmjs.org/ace-builds/-/ace-builds-1.43.3.tgz",
"integrity": "sha512-MCl9rALmXwIty/4Qboijo/yNysx1r6hBTzG+6n/TiOm5LFhZpEvEIcIITPFiEOEFDfgBOEmxu+a4f54LEFM6Sg=="
},
"node_modules/bootstrap": { "node_modules/bootstrap": {
"version": "5.3.8", "version": "5.3.8",
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.8.tgz", "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.8.tgz",

View file

@ -1,5 +1,6 @@
{ {
"dependencies": { "dependencies": {
"ace-builds": "^1.43.3",
"bootstrap": "^5.3.8" "bootstrap": "^5.3.8"
} }
} }

View file

@ -31,11 +31,12 @@ The file format is **JSON**:
* **prometheus_route** (string): Route to expose Prometheus metrics (default "/metrics") * **prometheus_route** (string): Route to expose Prometheus metrics (default "/metrics")
* **prometheus_notes_metric** (string): Name of the notes count metric (default "collerd_notes") * **prometheus_notes_metric** (string): Name of the notes count metric (default "collerd_notes")
* **observation_internal** (int): Number of seconds to wait between two observations (default 60) * **observation_internal** (int): Number of seconds to wait between two observations (default 60)
* **languages** ([]string): List of supported [languages](https://github.com/microsoft/monaco-editor/tree/main/src/basic-languages) * **languages** ([]string): List of supported [languages](https://github.com/ajaxorg/ace/tree/master/src/mode)
* **language** (string): Default language (default "text") * **language** (string): Default language (default "text")
* **enable_upload_file_button** (bool): Display the upload file button in the UI (default true) * **enable_upload_file_button** (bool): Display the upload file button in the UI (default true)
* **tls_cert_file** (string): Path to TLS certificate file to enable HTTPS * **tls_cert_file** (string): Path to TLS certificate file to enable HTTPS
* **tls_key_file** (string): Path to TLS key file to enable HTTPS * **tls_key_file** (string): Path to TLS key file to enable HTTPS
* **ace_directory** (string): Serve [Ace](hhttps://ace.c9.io/) assets from this local directory (ex: "./node_modules/ace-builds"). See **Dependencies** for details.
* **bootstrap_directory** (string): Serve [Bootstrap](https://getbootstrap.com/) assets from this local directory (ex: "./node_modules/bootstrap/dist"). See **Dependencies** for details. * **bootstrap_directory** (string): Serve [Bootstrap](https://getbootstrap.com/) assets from this local directory (ex: "./node_modules/bootstrap/dist"). See **Dependencies** for details.
The configuration file is not required but the service might not be exposed to the public. The configuration file is not required but the service might not be exposed to the public.
@ -85,12 +86,12 @@ Errors return **500 Server Internal Error** with the **JSON** payload:
The web interface depends on: The web interface depends on:
- [Ace](https://ace.c9.io/)
- [Bootstrap](https://getbootstrap.com/) - [Bootstrap](https://getbootstrap.com/)
- [Monaco Editor](https://github.com/microsoft/monaco-editor/)
By default, those dependencies are fetched from **remote CDN** services by the client. By default, those dependencies are fetched from **remote CDN** services by the client.
If you would like to download them to serve them locally: If you would like to download and serve them locally:
``` ```
npm install npm install
@ -106,8 +107,7 @@ Then configure the local directories:
```json ```json
{ {
"ace_directory": "./node_modules/ace-builds",
"bootstrap_directory": "./node_modules/bootstrap/dist" "bootstrap_directory": "./node_modules/bootstrap/dist"
} }
``` ```
Downloading Monaco Editor is not supported yet.

View file

@ -28,6 +28,7 @@ type Config struct {
EnableUploadFileButton bool `json:"enable_upload_file_button"` EnableUploadFileButton bool `json:"enable_upload_file_button"`
TLSCertFile string `json:"tls_cert_file"` TLSCertFile string `json:"tls_cert_file"`
TLSKeyFile string `json:"tls_key_file"` TLSKeyFile string `json:"tls_key_file"`
AceDirectory string `json:"ace_directory"`
BootstrapDirectory string `json:"bootstrap_directory"` BootstrapDirectory string `json:"bootstrap_directory"`
} }
@ -56,22 +57,21 @@ func NewConfig() *Config {
PrometheusNotesMetric: "collerd_notes", PrometheusNotesMetric: "collerd_notes",
ObservationInterval: 60, ObservationInterval: 60,
Languages: []string{ Languages: []string{
"Text", "css",
"CSS", "dockerfile",
"Dockerfile", "golang",
"Go", "html",
"HCL", "javascript",
"HTML", "json",
"Javascript", "markdown",
"JSON", "perl",
"Markdown", "python",
"Perl", "ruby",
"Python", "rust",
"Ruby", "sh",
"Rust", "sql",
"Shell", "text",
"SQL", "yaml",
"YAML",
}, },
Language: "text", Language: "text",
EnableUploadFileButton: true, EnableUploadFileButton: true,

View file

@ -21,11 +21,13 @@ type PageData struct {
Version string Version string
Expirations []int Expirations []int
Expiration int Expiration int
Language string
Languages []string Languages []string
Err error Err error
URL string URL string
Note *Note Note *Note
EnableUploadFileButton bool EnableUploadFileButton bool
AceDirectory string
BootstrapDirectory string BootstrapDirectory string
} }

View file

@ -113,7 +113,9 @@ func (s *Server) Start() error {
Title: s.config.Title, Title: s.config.Title,
Expirations: s.config.Expirations, Expirations: s.config.Expirations,
Expiration: s.config.Expiration, Expiration: s.config.Expiration,
Language: s.config.Language,
Languages: s.config.Languages, Languages: s.config.Languages,
AceDirectory: s.config.AceDirectory,
BootstrapDirectory: s.config.BootstrapDirectory, BootstrapDirectory: s.config.BootstrapDirectory,
} }
@ -159,6 +161,10 @@ func (s *Server) Start() error {
} }
r.Path("/{id:[a-zA-Z0-9]+}.html").Handler(webNoteHandler).Methods("GET") r.Path("/{id:[a-zA-Z0-9]+}.html").Handler(webNoteHandler).Methods("GET")
if s.config.AceDirectory != "" {
r.PathPrefix("/static/ace-builds/").Handler(http.StripPrefix("/static/ace-builds/", http.FileServer(http.Dir(s.config.AceDirectory))))
}
if s.config.BootstrapDirectory != "" { if s.config.BootstrapDirectory != "" {
r.PathPrefix("/static/bootstrap/").Handler(http.StripPrefix("/static/bootstrap/", http.FileServer(http.Dir(s.config.BootstrapDirectory)))) r.PathPrefix("/static/bootstrap/").Handler(http.StripPrefix("/static/bootstrap/", http.FileServer(http.Dir(s.config.BootstrapDirectory))))
} }
@ -168,10 +174,10 @@ func (s *Server) Start() error {
addr := fmt.Sprintf("%s:%d", s.config.ListenAddress, s.config.ListenPort) addr := fmt.Sprintf("%s:%d", s.config.ListenAddress, s.config.ListenPort)
if s.config.HasTLS() { if s.config.HasTLS() {
s.logger.Info(fmt.Sprintf("listening to %s:%d (https)", s.config.ListenAddress, s.config.ListenPort)) s.logger.Info(fmt.Sprintf("listening to %s:%d", s.config.ListenAddress, s.config.ListenPort), slog.Any("scheme", "http"))
return http.ListenAndServeTLS(addr, s.config.TLSCertFile, s.config.TLSKeyFile, r) return http.ListenAndServeTLS(addr, s.config.TLSCertFile, s.config.TLSKeyFile, r)
} else { } else {
s.logger.Info(fmt.Sprintf("listening to %s:%d (http)", s.config.ListenAddress, s.config.ListenPort)) s.logger.Info(fmt.Sprintf("listening to %s:%d", s.config.ListenAddress, s.config.ListenPort), slog.Any("scheme", "https"))
return http.ListenAndServe(addr, r) return http.ListenAndServe(addr, r)
} }
} }

View file

@ -40,15 +40,17 @@
<select class="form-select" aria-label="Expiration" id="expiration" name="expiration"> <select class="form-select" aria-label="Expiration" id="expiration" name="expiration">
<option disabled>Expiration</option> <option disabled>Expiration</option>
{{range $exp := .Expirations}} {{range $exp := .Expirations}}
<option {{ if eq $exp $.Expiration }}selected="selected"{{end}} value="{{$exp}}">{{HumanDuration $exp}}</option> <option {{ if eq $exp $.Expiration }}selected="selected" {{end}} value="{{$exp}}">
{{HumanDuration $exp}}</option>
{{end}} {{end}}
</select> </select>
</div> </div>
<div class="col"> <div class="col">
<select class="form-select" aria-label="Language" id="language" name="language"> <select class="form-select" aria-label="Language" id="language" name="language">
<option selected="selected" value="" disabled>Language</option> <option disabled>Language</option>
{{range .Languages}} {{range $language := .Languages}}
<option value="{{lower .}}">{{.}}</option> <option {{ if eq $language $.Language }}selected="selected" {{end}}value="{{lower .}}">
{{$language}}</option>
{{end}} {{end}}
</select> </select>
</div> </div>
@ -69,36 +71,36 @@
</div> </div>
</div> </div>
</div> </div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.52.2/min/vs/loader.min.js"></script> <script
src="{{if .AceDirectory}}/static/ace-builds{{else}}https://cdn.jsdelivr.net/npm/ace-builds@1.43.3{{end}}/src-noconflict/ace.js"
type="text/javascript" charset="utf-8"></script>
<script> <script>
require.config({ paths: { vs: 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.52.2/min/vs' } }); var editor = ace.edit("editor");
if (document.documentElement.getAttribute('data-bs-theme') == 'light') {
editor.setTheme("ace/theme/github_light_default");
} else {
editor.setTheme("ace/theme/github_dark");
}
require(['vs/editor/editor.main'], function () { // Syntax highlighting
var editor = monaco.editor.create(document.getElementById('editor'), { document.getElementById("language").addEventListener("change", (e) => {
theme: document.documentElement.getAttribute('data-bs-theme') == 'light' ? "vs" : "vs-dark", if (e.target.value != "") {
language: document.getElementById("language").value, editor.getSession().setMode("ace/mode/" + e.target.value);
}); }
});
// Syntax highlighting // Dark mode
document.getElementById("language").addEventListener("change", (e) => { document.getElementById("lightSwitch").addEventListener("click", () => {
if (e.target.value != "") { if (document.documentElement.getAttribute('data-bs-theme') == 'light') {
monaco.editor.setModelLanguage(editor.getModel(), e.target.value); editor.setTheme("ace/theme/github_light_default")
} } else if (document.documentElement.getAttribute('data-bs-theme') == 'dark') {
}); editor.setTheme("ace/theme/github_dark")
}
});
// Dark mode // Copy content on submit
document.getElementById("lightSwitch").addEventListener("click", () => { document.getElementById("form").addEventListener("formdata", (e) => {
if (document.documentElement.getAttribute('data-bs-theme') == 'light') { e.formData.append('content', editor.getValue());
monaco.editor.setTheme("vs")
} else if (document.documentElement.getAttribute('data-bs-theme') == 'dark') {
monaco.editor.setTheme("vs-dark")
}
});
// Copy content on submit
document.getElementById("form").addEventListener("formdata", (e) => {
e.formData.append('content', editor.getModel().getValue());
});
}); });
</script> </script>
</form> </form>

View file

@ -32,27 +32,31 @@
<div id="editor" name="editor" class="form-control" <div id="editor" name="editor" class="form-control"
style="height: 300px; resize: vertical; overflow: auto;"></div> style="height: 300px; resize: vertical; overflow: auto;"></div>
</div> </div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.52.2/min/vs/loader.min.js"></script> <script
src="{{if .AceDirectory}}/static/ace-builds{{else}}https://cdn.jsdelivr.net/npm/ace-builds@1.43.3{{end}}/src-noconflict/ace.js"
type="text/javascript" charset="utf-8"></script>
<script> <script>
require.config({ paths: { vs: 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.52.2/min/vs' } }); var editor = ace.edit("editor");
editor.setValue("{{string .Note.Content}}");
editor.setReadOnly(true);
editor.getSession().setMode("ace/mode/{{.Note.Language}}");
editor.getSession().selection.clearSelection();
require(['vs/editor/editor.main'], function () { if (document.documentElement.getAttribute('data-bs-theme') == 'light') {
var editor = monaco.editor.create(document.getElementById('editor'), { editor.setTheme("ace/theme/github_light_default");
theme: document.documentElement.getAttribute('data-bs-theme') == 'light' ? "vs" : "vs-dark", } else {
language: "{{.Note.Language}}", editor.setTheme("ace/theme/github_dark");
readOnly: true, }
value: "{{string .Note.Content}}"
});
// Dark mode // Dark mode
document.getElementById("lightSwitch").addEventListener("click", () => { document.getElementById("lightSwitch").addEventListener("click", () => {
if (document.documentElement.getAttribute('data-bs-theme') == 'light') { if (document.documentElement.getAttribute('data-bs-theme') == 'light') {
monaco.editor.setTheme("vs") editor.setTheme("ace/theme/github_light_default")
} else if (document.documentElement.getAttribute('data-bs-theme') == 'dark') { } else if (document.documentElement.getAttribute('data-bs-theme') == 'dark') {
monaco.editor.setTheme("vs-dark") editor.setTheme("ace/theme/github_dark")
} }
});
}); });
</script> </script>
</div> </div>
{{end}} {{end}}