Implement blocks based editor

This commit is contained in:
Peter Stuifzand 2019-08-25 12:30:00 +02:00
parent 0c0471b673
commit 219981913e
7 changed files with 332 additions and 34 deletions

32
editor.go Normal file
View File

@ -0,0 +1,32 @@
package main
import (
"bytes"
"html/template"
)
type editorJsJson struct {
Page string
ContentType string
Data template.JS
}
func renderEditor(pageName, inputText, contentType string) (template.HTML, error) {
if contentType == "json" {
t, err := template.ParseFiles("templates/editorjs.html")
if err != nil {
return "", err
}
if inputText == "" {
inputText = "null"
}
data := editorJsJson{Page: pageName, Data: template.JS(inputText), ContentType: contentType}
var buf bytes.Buffer
err = t.Execute(&buf, data)
if err != nil {
return "", err
}
return template.HTML(buf.String()), nil
}
return "", nil
}

19
file.go
View File

@ -3,8 +3,8 @@ package main
import ( import (
"bufio" "bufio"
"bytes" "bytes"
"encoding/json"
"fmt" "fmt"
"github.com/sergi/go-diff/diffmatchpatch"
"html" "html"
"html/template" "html/template"
"io/ioutil" "io/ioutil"
@ -14,6 +14,8 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"time" "time"
"github.com/sergi/go-diff/diffmatchpatch"
) )
type FilePages struct { type FilePages struct {
@ -44,7 +46,20 @@ func (fp *FilePages) Save(p string, page Page, summary, author string) error {
return err return err
} }
defer f.Close() defer f.Close()
f.WriteString(strings.Replace(page.Content, "\r\n", "\n", -1)) if page.Content[0] == '{' {
var buf bytes.Buffer
err = json.Indent(&buf, []byte(page.Content), "", " ")
if err != nil {
return err
}
_, err = buf.WriteTo(f)
if err != nil {
return err
}
} else {
f.WriteString(strings.Replace(page.Content, "\r\n", "\n", -1))
}
return saveWithGit(fp, p, summary, author) return saveWithGit(fp, p, summary, author)
} }

121
main.go
View File

@ -1,18 +1,20 @@
package main package main
import ( import (
"encoding/json"
"flag" "flag"
"fmt" "fmt"
"gitlab.com/golang-commonmark/markdown"
"html/template" "html/template"
"log" "log"
"net/http" "net/http"
"net/url" "net/url"
"p83.nl/go/ekster/pkg/util"
"p83.nl/go/indieauth"
"regexp" "regexp"
"strings" "strings"
"time" "time"
"gitlab.com/golang-commonmark/markdown"
"p83.nl/go/ekster/pkg/util"
"p83.nl/go/indieauth"
) )
var ( var (
@ -71,6 +73,7 @@ type editPage struct {
Title string Title string
Content string Content string
Name string Name string
Editor template.HTML
} }
type historyPage struct { type historyPage struct {
@ -280,7 +283,12 @@ func (h *saveHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return return
} }
r.ParseForm() err = r.ParseForm()
if err != nil {
http.Error(w, err.Error(), 500)
return
}
isJson := r.PostForm.Get("json") == "1"
page := r.PostForm.Get("p") page := r.PostForm.Get("p")
summary := r.PostForm.Get("summary") summary := r.PostForm.Get("summary")
@ -289,7 +297,12 @@ func (h *saveHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if err != nil { if err != nil {
log.Println(err) log.Println(err)
} }
http.Redirect(w, r, "/"+page, http.StatusFound)
if isJson {
fmt.Print(w, "{}")
} else {
http.Redirect(w, r, "/"+page, http.StatusFound)
}
} }
func (h *editHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (h *editHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
@ -318,10 +331,35 @@ func (h *editHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
pageText := mp.Get(page).Content pageText := mp.Get(page).Content
jsonEditor := pageText[0] == '{'
var editor template.HTML
if jsonEditor {
editor, err = renderEditor(page, pageText, "json")
if err != nil {
http.Error(w, err.Error(), 500)
return
}
} else {
editor = `
<textarea name="content" rows="24" style="width:100%">{{ .Content }}</textarea>
<br>
<div class="field">
<label class="label">Summary</label>
<div class="control">
<input type="text" name="summary" class="input" placeholder="Summary"/>
</div>
</div>
<button class="button is-primary" type="submit">Save</button>
`
}
data := editPage{ data := editPage{
Session: sess, Session: sess,
Title: strings.Replace(page, "_", " ", -1), Title: strings.Replace(page, "_", " ", -1),
Content: pageText, Content: pageText,
Editor: editor,
Name: page, Name: page,
} }
@ -362,24 +400,36 @@ func (h *indexHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return return
} }
hrefRE := regexp.MustCompile(`\[\[\s*([\w- ]+)\s*\]\]`) jsonPage := pageText[0] == '{'
if jsonPage {
pageText, err = renderJSON(pageText)
pageText = hrefRE.ReplaceAllStringFunc(pageText, func(s string) string { if err != nil {
s = strings.TrimPrefix(s, "[[") http.Error(w, err.Error(), 500)
s = strings.TrimSuffix(s, "]]") return
s = strings.TrimSpace(s)
if !mp.Exist(s) {
// return fmt.Sprintf("<a href=%q class=%q>%s</a>", s, "edit", s)
return fmt.Sprintf("%s[?](/%s)", s, strings.Replace(s, " ", "_", -1))
} }
return fmt.Sprintf("[%s](/%s)", s, strings.Replace(s, " ", "_", -1)) }
})
md := markdown.New( if !jsonPage {
markdown.HTML(true), hrefRE := regexp.MustCompile(`\[\[\s*([\w.\- ]+)\s*\]\]`)
markdown.XHTMLOutput(true),
) pageText = hrefRE.ReplaceAllStringFunc(pageText, func(s string) string {
pageText = md.RenderToString([]byte(pageText)) s = strings.TrimPrefix(s, "[[")
s = strings.TrimSuffix(s, "]]")
s = strings.TrimSpace(s)
if !mp.Exist(s) {
// return fmt.Sprintf("<a href=%q class=%q>%s</a>", s, "edit", s)
return fmt.Sprintf("%s[?](/%s)", s, strings.Replace(s, " ", "_", -1))
}
return fmt.Sprintf("[%s](/%s)", s, strings.Replace(s, " ", "_", -1))
})
md := markdown.New(
markdown.HTML(true),
markdown.XHTMLOutput(true),
)
pageText = md.RenderToString([]byte(pageText))
}
data := indexPage{ data := indexPage{
Session: sess, Session: sess,
@ -462,6 +512,22 @@ func (h *recentHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
} }
} }
type LinkResponseMetaImage struct {
Url string `json:"url"`
}
type LinkResponseMeta struct {
Title string `json:"title"`
Description string `json:"description"`
Image LinkResponseMetaImage `json:"image"`
}
type LinkResponse struct {
Success int `json:"success"`
Link string `json:"link"`
Meta LinkResponseMeta `json:"meta"`
}
func main() { func main() {
var port int var port int
flag.IntVar(&port, "port", 8080, "http port") flag.IntVar(&port, "port", 8080, "http port")
@ -470,7 +536,20 @@ func main() {
mp = NewFilePages("data") mp = NewFilePages("data")
http.Handle("/auth/", &authHandler{}) http.Handle("/auth/", &authHandler{})
http.Handle("/public/", http.StripPrefix("/public/", http.FileServer(http.Dir("ui/node_modules/trix/dist/")))) http.HandleFunc("/fetchLink", func(w http.ResponseWriter, r *http.Request) {
link := r.URL.Query().Get("url")
u, err := url.Parse(link)
if err != nil {
http.Error(w, err.Error(), 400)
return
}
var response LinkResponse
response.Success = 1
response.Link = u.String()
response.Meta.Title = "Test"
json.NewEncoder(w).Encode(response)
})
http.Handle("/public/", http.StripPrefix("/public/", http.FileServer(http.Dir("/home/peter/work/editorjs/dist/"))))
http.Handle("/save/", &saveHandler{}) http.Handle("/save/", &saveHandler{})
http.Handle("/edit/", &editHandler{}) http.Handle("/edit/", &editHandler{})
http.Handle("/history/", &historyHandler{}) http.Handle("/history/", &historyHandler{})

171
render.go Normal file
View File

@ -0,0 +1,171 @@
package main
import (
"bytes"
"encoding/json"
"fmt"
)
type Block struct {
Type string
Data json.RawMessage
}
type Paragraph struct {
Text string
}
type Code struct {
Code string
}
type List struct {
Style string
Items []string
}
type Header struct {
Level int
Text string
}
type ChecklistItem struct {
Text string
Checked bool
}
type Checklist struct {
Style string
Items []ChecklistItem
}
type Link struct {
Link string
Meta LinkResponseMeta
}
type Table struct {
Content [][]string
}
type Document struct {
Time int64
Version string
Blocks []Block
}
func renderJSON(text string) (string, error) {
var data Document
err := json.Unmarshal([]byte(text), &data)
if err != nil {
return "", err
}
var buf bytes.Buffer
for _, block := range data.Blocks {
switch block.Type {
case "table":
var table Table
err = json.Unmarshal(block.Data, &table)
if err != nil {
return "", fmt.Errorf("error while parsing %s: %s", block.Type, err.Error())
}
buf.WriteString("<table class='table'>")
for _, row := range table.Content {
buf.WriteString("<tr>")
for _, col := range row {
buf.WriteString("<td>")
buf.WriteString(col)
buf.WriteString("</td>")
}
buf.WriteString("</tr>")
}
buf.WriteString("</table>")
break
case "link":
// TODO(peter): improve link rendering
var link Link
err = json.Unmarshal(block.Data, &link)
if err != nil {
return "", fmt.Errorf("error while parsing %s: %s", block.Type, err.Error())
}
fmt.Fprintf(&buf, `<a href=%q>%s</a>`, link.Link, link.Meta.Title)
case "list":
var list List
err = json.Unmarshal(block.Data, &list)
if err != nil {
return "", fmt.Errorf("error while parsing %s: %s", block.Type, err.Error())
}
var tag string
if list.Style == "ordered" {
tag = "ol"
} else {
tag = "ul"
}
buf.WriteString("<")
buf.WriteString(tag)
buf.WriteString(">")
for _, item := range list.Items {
buf.WriteString("<li>")
buf.WriteString(item)
buf.WriteString("</li>")
}
buf.WriteString("</")
buf.WriteString(tag)
buf.WriteString(">")
case "header":
var header Header
err = json.Unmarshal(block.Data, &header)
if err != nil {
return "", fmt.Errorf("error while parsing %s: %s", block.Type, err.Error())
}
fmt.Fprintf(&buf, "<h%d>%s</h%d>", header.Level, header.Text, header.Level)
case "paragraph":
var para Paragraph
err = json.Unmarshal(block.Data, &para)
if err != nil {
return "", fmt.Errorf("error while parsing %s: %s", block.Type, err.Error())
}
buf.WriteString("<p>")
buf.WriteString(para.Text)
buf.WriteString("</p>")
case "code":
var code Code
err = json.Unmarshal(block.Data, &code)
if err != nil {
return "", fmt.Errorf("error while parsing %s: %s", block.Type, err.Error())
}
buf.WriteString("<pre>")
buf.WriteString(code.Code)
buf.WriteString("</pre>")
case "checklist":
var checklist Checklist
err = json.Unmarshal(block.Data, &checklist)
if err != nil {
return "", fmt.Errorf("error while parsing %s: %s", block.Type, err.Error())
}
for _, item := range checklist.Items {
buf.WriteString("<p>")
buf.WriteString(`<span class="icon is-medium">`)
if item.Checked {
buf.WriteString(` <span class="fa-stack">`)
buf.WriteString(` <i class="fa fa-circle fa-stack-2x has-text-success"></i>`)
buf.WriteString(` <i class="fa fa-check fa-stack-1x fa-inverse"></i>`)
buf.WriteString(` </span>`)
}
buf.WriteString(`</span>`)
buf.WriteString(item.Text)
buf.WriteString("</p>")
}
default:
return "", fmt.Errorf("unknown type: %s", block.Type)
}
fmt.Fprintln(&buf)
}
return buf.String(), nil
}

View File

@ -2,18 +2,12 @@
<h1 class="title">{{ .Title }}</h1> <h1 class="title">{{ .Title }}</h1>
<form action="/save/" method="post"> <form action="/save/" method="post">
<input type="hidden" name="p" value="{{ .Name }}" /> <input type="hidden" name="p" value="{{ .Name }}" />
<textarea name="content" rows="24" style="width:100%">{{ .Content }}</textarea> {{ .Editor }}
<br>
<div class="field">
<label class="label">Summary</label>
<div class="control">
<input type="text" name="summary" class="input" placeholder="Summary"/>
</div>
</div>
<button class="button is-primary" type="submit">Save</button>
</form> </form>
{{ end }} {{ end }}
{{ define "content_head" }} {{ define "content_head" }}
{{ end }} {{ end }}
{{ define "footer_scripts" }}
{{ end }}

2
templates/editorjs.html Normal file
View File

@ -0,0 +1,2 @@
<div id="editor" data-input="{{ .Data }}" data-saveurl="/save/" data-page="{{ .Page }}" save-type="{{ .ContentType }}"></div>
<script src="/public/index.bundle.js"></script>

View File

@ -6,6 +6,7 @@
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"> content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge"> <meta http-equiv="X-UA-Compatible" content="ie=edge">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.2/css/bulma.min.css" /> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.2/css/bulma.min.css" />
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" />
<title>{{ .Title }} - Wiki</title> <title>{{ .Title }} - Wiki</title>
{{ block "content_head" . }} {{ end }} {{ block "content_head" . }} {{ end }}
<style> <style>
@ -51,6 +52,10 @@
<section class="section"> <section class="section">
{{ template "content" . }} {{ template "content" . }}
</section> </section>
<div id="save-indicator" class="hidden"></div>
</div> </div>
{{ block "footer_scripts" . }}{{ end }}
</body> </body>
</html> </html>