Editor live preview

This commit is contained in:
Jan-Lukas Else 2021-11-01 18:39:08 +01:00
parent 53e90075f5
commit bbfc68d145
10 changed files with 156 additions and 36 deletions

View File

@ -10,6 +10,8 @@ import (
"net/url"
"strings"
"github.com/gorilla/websocket"
"github.com/microcosm-cc/bluemonday"
"go.goblog.app/app/pkgs/contenttype"
"gopkg.in/yaml.v3"
)
@ -24,6 +26,51 @@ func (a *goBlog) serveEditor(w http.ResponseWriter, r *http.Request) {
})
}
var upgrader = websocket.Upgrader{
EnableCompression: true,
}
func (a *goBlog) serveEditorPreview(w http.ResponseWriter, r *http.Request) {
c, err := upgrader.Upgrade(w, r, nil)
if err != nil {
return
}
defer c.Close()
for {
// Retrieve content
mt, message, err := c.ReadMessage()
if err != nil {
break
}
// Create preview
preview, err := a.createMarkdownPreview(message)
if err != nil {
continue
}
// Write preview to socket
err = c.WriteMessage(mt, preview)
if err != nil {
break
}
}
}
func (a *goBlog) createMarkdownPreview(markdown []byte) (rendered []byte, err error) {
mdString := string(markdown)
if split := strings.Split(mdString, "---\n"); len(split) >= 3 && len(strings.TrimSpace(split[0])) == 0 {
// Remove frontmatter from content
mdString = strings.Join(split[2:], "---\n")
}
// Render markdown
rendered, err = a.renderMarkdown(mdString, true)
if err != nil {
return nil, err
}
// Sanitize HTML
rendered = bluemonday.UGCPolicy().SanitizeBytes(rendered)
return
}
func (a *goBlog) serveEditorPost(w http.ResponseWriter, r *http.Request) {
blog := r.Context().Value(blogKey).(string)
if action := r.FormValue("editoraction"); action != "" {

View File

@ -8,12 +8,11 @@ import (
"net/http/httptest"
"net/http/httputil"
"net/url"
"path"
"strings"
"go.goblog.app/app/pkgs/contenttype"
)
//go:embed leaflet/*
var leafletFiles embed.FS
const defaultGeoMapPath = "/map"
func (a *goBlog) serveGeoMap(w http.ResponseWriter, r *http.Request) {
@ -75,31 +74,6 @@ func (a *goBlog) serveGeoMap(w http.ResponseWriter, r *http.Request) {
})
}
//go:embed leaflet/*
var leafletFiles embed.FS
func (a *goBlog) serveLeaflet(basePath string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
fileName := strings.TrimPrefix(r.URL.Path, basePath)
fb, err := leafletFiles.ReadFile(fileName)
if err != nil {
a.serve404(w, r)
return
}
switch path.Ext(fileName) {
case ".js":
w.Header().Set(contentType, contenttype.JS)
_, _ = a.min.Write(w, contenttype.JSUTF8, fb)
case ".css":
w.Header().Set(contentType, contenttype.CSS)
_, _ = a.min.Write(w, contenttype.CSSUTF8, fb)
default:
w.Header().Set(contentType, http.DetectContentType(fb))
_, _ = w.Write(fb)
}
}
}
func (a *goBlog) proxyTiles(basePath string) http.HandlerFunc {
osmUrl, _ := url.Parse("https://tile.openstreetmap.org/")
tileProxy := http.StripPrefix(basePath, httputil.NewSingleHostReverseProxy(osmUrl))

4
go.mod
View File

@ -20,6 +20,7 @@ require (
github.com/gorilla/handlers v1.5.1
github.com/gorilla/securecookie v1.1.1
github.com/gorilla/sessions v1.2.1
github.com/gorilla/websocket v1.4.2
github.com/jlaffaye/ftp v0.0.0-20211029032751-b1140299f4df
// master
github.com/jlelse/feeds v1.2.1-0.20210704161900-189f94254ad4
@ -47,7 +48,6 @@ require (
golang.org/x/net v0.0.0-20211029224645-99673261e6eb
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
// main
tailscale.com v1.16.2
willnorris.com/go/microformats v1.1.1
)
@ -100,7 +100,7 @@ require (
go4.org/intern v0.0.0-20210108033219-3eb7198706b2 // indirect
go4.org/mem v0.0.0-20201119185036-c04c5a6ff174 // indirect
go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222180813-1025295fd063 // indirect
golang.org/x/sys v0.0.0-20211030160813-b3129d9d1021 // indirect
golang.org/x/sys v0.0.0-20211031064116-611d5d643895 // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/time v0.0.0-20210611083556-38a9dc6acbc6 // indirect
golang.zx2c4.com/wireguard v0.0.0-20210905140043-2ef39d47540c // indirect

6
go.sum
View File

@ -240,6 +240,8 @@ github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyC
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/hashicorp/consul/api v1.10.1/go.mod h1:XjsvQN+RJGWI2TWy1/kqaE16HrR2J/FWgkYjdZQsX9M=
github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms=
@ -683,8 +685,8 @@ golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211030160813-b3129d9d1021 h1:giLT+HuUP/gXYrG2Plg9WTjj4qhfgaW424ZIFog3rlk=
golang.org/x/sys v0.0.0-20211030160813-b3129d9d1021/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211031064116-611d5d643895 h1:iaNpwpnrgL5jzWS0vCNnfa8HqzxveCFpFx3uC/X4Tps=
golang.org/x/sys v0.0.0-20211031064116-611d5d643895/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210503060354-a79de5458b56 h1:b8jxX3zqjpqb2LklXPzKSGJhzyxCOZSz8ncv8Nv+y7w=

32
httpFs.go Normal file
View File

@ -0,0 +1,32 @@
package main
import (
"embed"
"net/http"
"path"
"strings"
"go.goblog.app/app/pkgs/contenttype"
)
func (a *goBlog) serveFs(fs embed.FS, basePath string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
fileName := strings.TrimPrefix(r.URL.Path, basePath)
fb, err := fs.ReadFile(fileName)
if err != nil {
a.serve404(w, r)
return
}
switch path.Ext(fileName) {
case ".js":
w.Header().Set(contentType, contenttype.JS)
_, _ = a.min.Write(w, contenttype.JSUTF8, fb)
case ".css":
w.Header().Set(contentType, contenttype.CSS)
_, _ = a.min.Write(w, contenttype.CSSUTF8, fb)
default:
w.Header().Set(contentType, http.DetectContentType(fb))
_, _ = w.Write(fb)
}
}
}

View File

@ -339,6 +339,7 @@ func (a *goBlog) blogEditorRouter(conf *configBlog) func(r chi.Router) {
r.Get("/unlisted", a.serveUnlisted)
r.Get("/unlisted"+feedPath, a.serveUnlisted)
r.Get("/unlisted"+paginationPath, a.serveUnlisted)
r.HandleFunc("/preview", a.serveEditorPreview)
}
}
@ -403,7 +404,7 @@ func (a *goBlog) blogGeoMapRouter(conf *configBlog) func(r chi.Router) {
r.Use(a.privateModeHandler)
r.Group(func(r chi.Router) {
r.With(a.cacheMiddleware).Get("/", a.serveGeoMap)
r.With(cacheLoggedIn, a.cacheMiddleware).HandleFunc("/leaflet/*", a.serveLeaflet(mapPath+"/"))
r.With(cacheLoggedIn, a.cacheMiddleware).HandleFunc("/leaflet/*", a.serveFs(leafletFiles, mapPath+"/"))
})
r.Get("/tiles/{z}/{x}/{y}.png", a.proxyTiles(mapPath+"/tiles"))
})

View File

@ -237,6 +237,12 @@ details summary {
}
}
.preview {
padding: 10px;
@include color-border(border, 1px, solid, primary);
margin-bottom: 5px;
}
#map {
height: 400px;
}

View File

@ -194,6 +194,13 @@ details summary > *:first-child {
background: var(--background, #fff);
}
.preview {
padding: 10px;
border: 1px solid #000;
border: 1px solid var(--primary, #000);
margin-bottom: 5px;
}
#map {
height: 400px;
}

View File

@ -0,0 +1,43 @@
(function () {
Array.from(document.querySelectorAll('.mdpreview')).forEach(element => {
// Get preview container
let previewContainer = document.getElementById(element.dataset.preview)
if (!previewContainer) {
return
}
// Get websocket path
let wsUrl = element.dataset.previewws
if (!wsUrl) {
return
}
// Create and open websocket
let ws = new WebSocket(((window.location.protocol === "https:") ? "wss://" : "ws://") + window.location.host + wsUrl)
ws.onopen = function () {
console.log("Preview-Websocket opened")
previewContainer.classList.add('preview')
previewContainer.classList.remove('hide')
if (ws) {
ws.send(element.value)
}
}
ws.onclose = function () {
console.log("Preview-Websocket closed")
previewContainer.classList.add('hide')
previewContainer.classList.remove('preview')
ws = null
}
ws.onmessage = function (evt) {
// Set preview HTML
previewContainer.innerHTML = evt.data
}
ws.onerror = function (evt) {
console.log("Preview-Websocket error: " + evt.data)
}
// Add listener
element.addEventListener('input', function () {
if (ws) {
ws.send(element.value)
}
})
})
})()

View File

@ -9,22 +9,27 @@
{{ md (editorpostdesc .BlogString) }}
<form class="fw p" method="post">
<input type="hidden" name="h" value="entry">
<textarea name="content" class="monospace h400p formcache" id="create-input">{{ editortemplate .BlogString }}</textarea>
<textarea name="content" class="monospace h400p formcache mdpreview" id="create-input" data-preview="post-preview" data-previewws="{{ .Blog.RelativePath "/editor/preview" }}">{{ editortemplate .BlogString }}</textarea>
<div id="post-preview" class="hide"></div>
<input type="submit" value="{{ string .Blog.Lang "create" }}">
</form>
{{ if .Data.UpdatePostURL }}
<h2 id="update">{{ string .Blog.Lang "update" }}</h2>
<form class="fw p" method="post" action="#update">
<input type="hidden" name="editoraction" value="updatepost">
<input type="hidden" name="url" value="{{ .Data.UpdatePostURL }}">
<textarea name="content" class="monospace h400p">{{ .Data.UpdatePostContent }}</textarea>
<textarea name="content" class="monospace h400p mdpreview" data-preview="update-preview" data-previewws="{{ .Blog.RelativePath "/editor/preview" }}">{{ .Data.UpdatePostContent }}</textarea>
<div id="update-preview" class="hide"></div>
<input type="submit" value="{{ string .Blog.Lang "update" }}">
</form>
{{ end }}
<h2>{{ string .Blog.Lang "posts" }}</h2>
<p><a href="{{ .Blog.RelativePath "/editor/drafts" }}">{{ string .Blog.Lang "drafts" }}</a></p>
<p><a href="{{ .Blog.RelativePath "/editor/private" }}">{{ string .Blog.Lang "privateposts" }}</a></p>
<p><a href="{{ .Blog.RelativePath "/editor/unlisted" }}">{{ string .Blog.Lang "unlistedposts" }}</a></p>
<h2>{{ string .Blog.Lang "upload" }}</h2>
<form class="fw p" method="post" enctype="multipart/form-data">
<input type="hidden" name="editoraction" value="upload">
@ -32,11 +37,14 @@
<input type="submit" value="{{ string .Blog.Lang "upload" }}">
</form>
<p><a href="{{ .Blog.RelativePath "/editor/files" }}">{{ string .Blog.Lang "mediafiles" }}</a></p>
<h2>{{ string .Blog.Lang "location" }}</h2>
<form class="fw p">
<input id="geobtn" type="button" value="{{ string .Blog.Lang "locationget" }}" data-failed="{{ string .Blog.Lang "locationfailed" }}" data-notsupported="{{ string .Blog.Lang "locationnotsupported" }}">
<input id="geostatus" type="text" class="hide" readonly>
</form>
<script defer src="{{ asset "js/mdpreview.js" }}"></script>
<script defer src="{{ asset "js/geohelper.js" }}"></script>
<script defer src="{{ asset "js/formcache.js" }}"></script>
</main>