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
|
||||||
|
}
|
19
file.go
19
file.go
|
@ -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
121
main.go
|
@ -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
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>
|
<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
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">
|
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>
|
||||||
|
|
Loading…
Reference in New Issue
Block a user