Implement blocks based editor
This commit is contained in:
parent
0c0471b673
commit
219981913e
32
editor.go
Normal file
32
editor.go
Normal 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
|
||||
}
|
17
file.go
17
file.go
|
@ -3,8 +3,8 @@ package main
|
|||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/sergi/go-diff/diffmatchpatch"
|
||||
"html"
|
||||
"html/template"
|
||||
"io/ioutil"
|
||||
|
@ -14,6 +14,8 @@ import (
|
|||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/sergi/go-diff/diffmatchpatch"
|
||||
)
|
||||
|
||||
type FilePages struct {
|
||||
|
@ -44,7 +46,20 @@ func (fp *FilePages) Save(p string, page Page, summary, author string) error {
|
|||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
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)
|
||||
}
|
||||
|
|
91
main.go
91
main.go
|
@ -1,18 +1,20 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"gitlab.com/golang-commonmark/markdown"
|
||||
"html/template"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"p83.nl/go/ekster/pkg/util"
|
||||
"p83.nl/go/indieauth"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gitlab.com/golang-commonmark/markdown"
|
||||
"p83.nl/go/ekster/pkg/util"
|
||||
"p83.nl/go/indieauth"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -71,6 +73,7 @@ type editPage struct {
|
|||
Title string
|
||||
Content string
|
||||
Name string
|
||||
Editor template.HTML
|
||||
}
|
||||
|
||||
type historyPage struct {
|
||||
|
@ -280,7 +283,12 @@ func (h *saveHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||
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")
|
||||
summary := r.PostForm.Get("summary")
|
||||
|
||||
|
@ -289,8 +297,13 @@ func (h *saveHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
|
||||
if isJson {
|
||||
fmt.Print(w, "{}")
|
||||
} else {
|
||||
http.Redirect(w, r, "/"+page, http.StatusFound)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *editHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
|
@ -318,10 +331,35 @@ func (h *editHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
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{
|
||||
Session: sess,
|
||||
Title: strings.Replace(page, "_", " ", -1),
|
||||
Content: pageText,
|
||||
Editor: editor,
|
||||
Name: page,
|
||||
}
|
||||
|
||||
|
@ -362,7 +400,18 @@ func (h *indexHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
hrefRE := regexp.MustCompile(`\[\[\s*([\w- ]+)\s*\]\]`)
|
||||
jsonPage := pageText[0] == '{'
|
||||
if jsonPage {
|
||||
pageText, err = renderJSON(pageText)
|
||||
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if !jsonPage {
|
||||
hrefRE := regexp.MustCompile(`\[\[\s*([\w.\- ]+)\s*\]\]`)
|
||||
|
||||
pageText = hrefRE.ReplaceAllStringFunc(pageText, func(s string) string {
|
||||
s = strings.TrimPrefix(s, "[[")
|
||||
|
@ -380,6 +429,7 @@ func (h *indexHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||
markdown.XHTMLOutput(true),
|
||||
)
|
||||
pageText = md.RenderToString([]byte(pageText))
|
||||
}
|
||||
|
||||
data := indexPage{
|
||||
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() {
|
||||
var port int
|
||||
flag.IntVar(&port, "port", 8080, "http port")
|
||||
|
@ -470,7 +536,20 @@ func main() {
|
|||
mp = NewFilePages("data")
|
||||
|
||||
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("/edit/", &editHandler{})
|
||||
http.Handle("/history/", &historyHandler{})
|
||||
|
|
171
render.go
Normal file
171
render.go
Normal 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, ¶)
|
||||
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
|
||||
}
|
|
@ -2,18 +2,12 @@
|
|||
<h1 class="title">{{ .Title }}</h1>
|
||||
<form action="/save/" method="post">
|
||||
<input type="hidden" name="p" value="{{ .Name }}" />
|
||||
<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>
|
||||
{{ .Editor }}
|
||||
</form>
|
||||
{{ end }}
|
||||
|
||||
{{ define "content_head" }}
|
||||
{{ end }}
|
||||
|
||||
{{ define "footer_scripts" }}
|
||||
{{ end }}
|
||||
|
|
2
templates/editorjs.html
Normal file
2
templates/editorjs.html
Normal 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>
|
|
@ -6,6 +6,7 @@
|
|||
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">
|
||||
<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>
|
||||
{{ block "content_head" . }} {{ end }}
|
||||
<style>
|
||||
|
@ -51,6 +52,10 @@
|
|||
<section class="section">
|
||||
{{ template "content" . }}
|
||||
</section>
|
||||
|
||||
<div id="save-indicator" class="hidden"></div>
|
||||
</div>
|
||||
{{ block "footer_scripts" . }}{{ end }}
|
||||
|
||||
</body>
|
||||
</html>
|
Loading…
Reference in New Issue
Block a user