feat: replace Monaco by Ace #32

Closed
jriou wants to merge 3 commits from dev/julien/ace into main
9 changed files with 114 additions and 73 deletions

6
package-lock.json generated
View file

@ -5,6 +5,7 @@
"packages": {
"": {
"dependencies": {
"ace-builds": "^1.43.3",
"bootstrap": "^5.3.8"
}
},
@ -18,6 +19,11 @@
"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": {
"version": "5.3.8",
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.8.tgz",

View file

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

View file

@ -31,12 +31,14 @@ The file format is **JSON**:
* **prometheus_route** (string): Route to expose Prometheus metrics (default "/metrics")
* **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)
* **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")
* **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_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.
* **disable_editor** (bool): Disable Ace editor.
The configuration file is not required but the service might not be exposed to the public.
@ -85,12 +87,12 @@ Errors return **500 Server Internal Error** with the **JSON** payload:
The web interface depends on:
- [Ace](https://ace.c9.io/)
- [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.
If you would like to download them to serve them locally:
If you would like to download and serve them locally:
```
npm install
@ -106,8 +108,7 @@ Then configure the local directories:
```json
{
"ace_directory": "./node_modules/ace-builds",
"bootstrap_directory": "./node_modules/bootstrap/dist"
}
```
Downloading Monaco Editor is not supported yet.
```

View file

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

View file

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

View file

@ -114,8 +114,11 @@ func (s *Server) Start() error {
Title: s.config.Title,
Expirations: s.config.Expirations,
Expiration: s.config.Expiration,
Language: s.config.Language,
Languages: s.config.Languages,
AceDirectory: s.config.AceDirectory,
BootstrapDirectory: s.config.BootstrapDirectory,
DisableEditor: s.config.DisableEditor,
}
if s.config.ShowVersion {
@ -160,6 +163,10 @@ func (s *Server) Start() error {
}
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 != "" {
r.PathPrefix("/static/bootstrap/").Handler(http.StripPrefix("/static/bootstrap/", http.FileServer(http.Dir(s.config.BootstrapDirectory))))
}
@ -169,10 +176,10 @@ func (s *Server) Start() error {
addr := fmt.Sprintf("%s:%d", s.config.ListenAddress, s.config.ListenPort)
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)
} 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)
}
}

View file

@ -4,6 +4,8 @@
<title>{{.Title}}</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="{{if ne .BootstrapDirectory ``}}/static/bootstrap/css/bootstrap.min.css{{else}}https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css{{end}}" rel="stylesheet">
<link
href="{{if .BootstrapDirectory}}/static/bootstrap/css/bootstrap.min.css{{else}}https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css{{end}}"
rel="stylesheet">
</head>
{{end}}

View file

@ -40,15 +40,17 @@
<select class="form-select" aria-label="Expiration" id="expiration" name="expiration">
<option disabled>Expiration</option>
{{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}}
</select>
</div>
<div class="col">
<select class="form-select" aria-label="Language" id="language" name="language">
<option selected="selected" value="" disabled>Language</option>
{{range .Languages}}
<option value="{{lower .}}">{{.}}</option>
<option disabled>Language</option>
{{range $language := .Languages}}
<option {{ if eq $language $.Language }}selected="selected" {{end}}value="{{lower .}}">
{{$language}}</option>
{{end}}
</select>
</div>
@ -56,11 +58,15 @@
</div>
<div class="container mb-4">
{{if .DisableEditor}}
<textarea class="form-control" id="content" name="content" cols="40" rows="12"></textarea>
{{else}}
<div class="row">
<div id="editor" name="editor" class="form-control"
style="height: 300px; resize: vertical; overflow: auto;"></div>
<input type="hidden" id="content" />
</div>
{{end}}
</div>
<div class="container mb-4">
<div class="row text-center justify-content-center">
@ -69,38 +75,40 @@
</div>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.52.2/min/vs/loader.min.js"></script>
{{if eq false .DisableEditor}}
<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>
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 () {
var editor = monaco.editor.create(document.getElementById('editor'), {
theme: document.documentElement.getAttribute('data-bs-theme') == 'light' ? "vs" : "vs-dark",
language: document.getElementById("language").value,
});
// Syntax highlighting
document.getElementById("language").addEventListener("change", (e) => {
if (e.target.value != "") {
editor.getSession().setMode("ace/mode/" + e.target.value);
}
});
// Syntax highlighting
document.getElementById("language").addEventListener("change", (e) => {
if (e.target.value != "") {
monaco.editor.setModelLanguage(editor.getModel(), e.target.value);
}
});
// Dark mode
document.getElementById("lightSwitch").addEventListener("click", () => {
if (document.documentElement.getAttribute('data-bs-theme') == 'light') {
editor.setTheme("ace/theme/github_light_default")
} else if (document.documentElement.getAttribute('data-bs-theme') == 'dark') {
editor.setTheme("ace/theme/github_dark")
}
});
// Dark mode
document.getElementById("lightSwitch").addEventListener("click", () => {
if (document.documentElement.getAttribute('data-bs-theme') == 'light') {
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());
});
// Copy content on submit
document.getElementById("form").addEventListener("formdata", (e) => {
e.formData.append('content', editor.getValue());
});
</script>
{{end}}
</form>
{{block "footer" .}}{{end}}

View file

@ -33,32 +33,44 @@
{{end}}
</ul>
</div>
{{if .DisableEditor}}
<div class="row">
<pre class="border px-3 pt-3" style="min-height: 300px; resize: vertical; overflow: auto;">
{{string .Note.Content}}
</pre>
</div>
{{else}}
<div class="row">
<div id="editor" name="editor" class="form-control"
style="height: 300px; resize: vertical; overflow: auto;"></div>
style="min-height: 300px; resize: vertical; overflow: auto;"></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>
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();
editor.setOptions({maxLines: Infinity});
require(['vs/editor/editor.main'], function () {
var editor = monaco.editor.create(document.getElementById('editor'), {
theme: document.documentElement.getAttribute('data-bs-theme') == 'light' ? "vs" : "vs-dark",
language: "{{.Note.Language}}",
readOnly: true,
value: "{{string .Note.Content}}"
});
if (document.documentElement.getAttribute('data-bs-theme') == 'light') {
editor.setTheme("ace/theme/github_light_default");
} else {
editor.setTheme("ace/theme/github_dark");
}
// Dark mode
document.getElementById("lightSwitch").addEventListener("click", () => {
if (document.documentElement.getAttribute('data-bs-theme') == 'light') {
monaco.editor.setTheme("vs")
} else if (document.documentElement.getAttribute('data-bs-theme') == 'dark') {
monaco.editor.setTheme("vs-dark")
}
});
// Dark mode
document.getElementById("lightSwitch").addEventListener("click", () => {
if (document.documentElement.getAttribute('data-bs-theme') == 'light') {
editor.setTheme("ace/theme/github_light_default")
} else if (document.documentElement.getAttribute('data-bs-theme') == 'dark') {
editor.setTheme("ace/theme/github_dark")
}
});
</script>
{{end}}
</div>
{{end}}