Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
43d44a4518 | ||
|
|
8bf7503c8e | ||
|
|
785215bd3c | ||
|
|
952e04815a | ||
|
|
d6c95a536c | ||
|
|
9785ef5e1e | ||
|
|
3b7aa70e44 |
File diff suppressed because one or more lines are too long
0
config/rotate.py
Normal file → Executable file
0
config/rotate.py
Normal file → Executable file
Submodule config/water.css updated: 7e86e4f67c...576eee5b82
61
notes/comment.go
Executable file
61
notes/comment.go
Executable file
@@ -0,0 +1,61 @@
|
||||
package notes
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"local/notes-server/filetree"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func (n *Notes) Comment(urlPath string, lineno int, comment string) error {
|
||||
p := filetree.NewPathFromURL(urlPath)
|
||||
if stat, err := os.Stat(p.Local); err != nil {
|
||||
return errors.New("cannot comment as it does not exist")
|
||||
} else if stat.IsDir() {
|
||||
return errors.New("cannot comment on a dir")
|
||||
}
|
||||
f, err := os.Open(p.Local)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
f2, err := ioutil.TempFile(os.TempDir(), path.Base(p.Local)+".*")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f2.Close()
|
||||
reader := bufio.NewReader(f)
|
||||
writer := io.Writer(f2)
|
||||
for i := 0; i < lineno+1; i++ {
|
||||
line, _, err := reader.ReadLine()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = io.Copy(writer, bytes.NewReader(line))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = writer.Write([]byte("\n"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
formatted := "\n"
|
||||
formatted += fmt.Sprintf("> *%s*\n", comment)
|
||||
_, err = io.Copy(writer, strings.NewReader(formatted))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = io.Copy(writer, reader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f2.Close()
|
||||
return os.Rename(f2.Name(), p.Local)
|
||||
}
|
||||
56
notes/comment_test.go
Normal file
56
notes/comment_test.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package notes
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"local/notes-server/config"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestComment(t *testing.T) {
|
||||
d, err := ioutil.TempDir(os.TempDir(), "testComment.*")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(d)
|
||||
f, err := ioutil.TempFile(d, "testFile.*")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
f.Write([]byte(`
|
||||
hello
|
||||
world
|
||||
# i have a heading
|
||||
## i have a subber heading
|
||||
### i have a[heading with](http://google.com) a hyperlink
|
||||
## *I think this heading is in italics*
|
||||
`))
|
||||
f.Close()
|
||||
fpath := path.Join("/comment", strings.TrimPrefix(f.Name(), d))
|
||||
config.Root = d
|
||||
n := &Notes{}
|
||||
t.Logf("d=%s, fpath=%s", d, fpath)
|
||||
|
||||
if err := n.Comment("/comment/a", 5, "a"); err == nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if err := n.Comment(fpath, -1, "illegal line no"); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if err := n.Comment(fpath, 10000, "big line no"); err == nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if err := n.Comment(fpath, 0, "first_line_OK"); err != nil {
|
||||
t.Error(err)
|
||||
} else if b, err := ioutil.ReadFile(f.Name()); err != nil {
|
||||
t.Error(err)
|
||||
} else if !bytes.Contains(b, []byte("> *first_line_OK*\n")) {
|
||||
t.Errorf("%s", b)
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,20 @@
|
||||
package notes
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"local/notes-server/filetree"
|
||||
"log"
|
||||
"path"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/gomarkdown/markdown"
|
||||
"github.com/gomarkdown/markdown/ast"
|
||||
"github.com/gomarkdown/markdown/html"
|
||||
"github.com/gomarkdown/markdown/parser"
|
||||
)
|
||||
@@ -18,8 +27,80 @@ func (n *Notes) File(urlPath string) (string, error) {
|
||||
b, _ := ioutil.ReadFile(p.Local)
|
||||
renderer := html.NewRenderer(html.RendererOptions{
|
||||
Flags: html.CommonFlags | html.TOC,
|
||||
RenderNodeHook: n.commentFormer(urlPath, b),
|
||||
})
|
||||
parser := parser.NewWithExtensions(parser.CommonExtensions | parser.HeadingIDs | parser.AutoHeadingIDs | parser.Titleblock)
|
||||
content := markdown.ToHTML(b, parser, renderer)
|
||||
return string(content) + "\n", nil
|
||||
}
|
||||
|
||||
func (n *Notes) commentFormer(urlPath string, md []byte) html.RenderNodeFunc {
|
||||
urlPath = strings.TrimPrefix(urlPath, "/")
|
||||
urlPath = strings.TrimPrefix(urlPath, strings.Split(urlPath, "/")[0])
|
||||
lines := bytes.Split(md, []byte("\n"))
|
||||
cur := -1
|
||||
nextHeader := func() {
|
||||
cur++
|
||||
for cur < len(lines) {
|
||||
if bytes.Contains(lines[cur], []byte("```")) {
|
||||
cur++
|
||||
for cur < len(lines) && !bytes.Contains(lines[cur], []byte("```")) {
|
||||
cur++
|
||||
}
|
||||
cur++
|
||||
}
|
||||
if cur >= len(lines) {
|
||||
break
|
||||
}
|
||||
line := lines[cur]
|
||||
if ok, err := regexp.Match(`^\s*#+\s*[^\s]+\s*$`, line); err != nil {
|
||||
panic(err)
|
||||
} else if ok {
|
||||
return
|
||||
}
|
||||
cur++
|
||||
}
|
||||
}
|
||||
return func(w io.Writer, node ast.Node, entering bool) (ast.WalkStatus, bool) {
|
||||
if heading, ok := node.(*ast.Heading); ok && !entering {
|
||||
log.Printf("%+v", heading)
|
||||
nextHeader()
|
||||
fmt.Fprintf(w, `
|
||||
<form method="POST" action=%q class="comment">
|
||||
<input name="lineno" type="number" style="display:none" value="%d"/>
|
||||
<input autocomplete="off" name="content" type="text"/>
|
||||
<input type="submit"/>
|
||||
</form>
|
||||
`, path.Join("/comment", urlPath)+"#"+heading.HeadingID, cur)
|
||||
}
|
||||
return ast.GoToNext, false
|
||||
}
|
||||
}
|
||||
|
||||
func (n *Notes) commentFormerOld() html.RenderNodeFunc {
|
||||
return func(w io.Writer, node ast.Node, entering bool) (ast.WalkStatus, bool) {
|
||||
if heading, ok := node.(*ast.Heading); ok {
|
||||
if !entering {
|
||||
literal := ""
|
||||
ast.WalkFunc(heading, func(n ast.Node, e bool) ast.WalkStatus {
|
||||
if leaf := n.AsLeaf(); e && leaf != nil {
|
||||
if literal != "" {
|
||||
literal += ".*"
|
||||
}
|
||||
literal += string(leaf.Literal)
|
||||
}
|
||||
return ast.GoToNext
|
||||
})
|
||||
level := heading.Level
|
||||
id := base64.URLEncoding.EncodeToString([]byte(fmt.Sprintf(`^[ \t]*%s\s*%s(].*)?\s*$`, strings.Repeat("#", level), literal)))
|
||||
fmt.Fprintf(w, `
|
||||
<form method="POST" target="/comment" name="%s" class="comment">
|
||||
<input autocomplete="off" name="content" type="text"/>
|
||||
<input type="submit"/>
|
||||
</form>
|
||||
`, id)
|
||||
}
|
||||
}
|
||||
return ast.GoToNext, false
|
||||
}
|
||||
}
|
||||
|
||||
37
server/comment.go
Executable file
37
server/comment.go
Executable file
@@ -0,0 +1,37 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"html"
|
||||
"local/notes-server/filetree"
|
||||
"log"
|
||||
"net/http"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func (s *Server) comment(w http.ResponseWriter, r *http.Request) {
|
||||
log.Println("COMMAND", r.Method, r.FormValue("lineno"), r.FormValue("content"))
|
||||
if r.Method != "POST" {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
linenos := r.FormValue("lineno")
|
||||
lineno, err := strconv.Atoi(linenos)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
comment := r.FormValue("content")
|
||||
comment = html.UnescapeString(comment)
|
||||
comment = strings.ReplaceAll(comment, "\r", "")
|
||||
|
||||
err = s.Notes.Comment(r.URL.Path, lineno, comment)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
url := *r.URL
|
||||
url.Path = path.Join("/notes/", filetree.NewPathFromURL(r.URL.Path).BaseHREF)
|
||||
http.Redirect(w, r, url.String(), http.StatusSeeOther)
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"local/notes-server/config"
|
||||
"local/notes-server/filetree"
|
||||
"net/http"
|
||||
"path"
|
||||
@@ -58,18 +59,27 @@ func fileHead(w http.ResponseWriter, baseHREF string) {
|
||||
}
|
||||
|
||||
func htmlEdit(w http.ResponseWriter, baseHREF string) {
|
||||
if config.ReadOnly {
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(w, `<div style='display:inline-block'>
|
||||
<a href=%q><input type="button" value="Edit"></input></a>
|
||||
</div><br>`, path.Join("/edit/", baseHREF))
|
||||
}
|
||||
|
||||
func htmlDelete(w http.ResponseWriter, baseHREF string) {
|
||||
if config.ReadOnly {
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(w, `<div style='display:inline-block'>
|
||||
<a href=%q><input type="button" value="Delete" onclick="return confirm('Delete?');"></input></a>
|
||||
</div><br>`, path.Join("/delete/", baseHREF))
|
||||
}
|
||||
|
||||
func htmlCreate(w http.ResponseWriter, baseHREF string) {
|
||||
if config.ReadOnly {
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(w, `
|
||||
<form action=%q method="get">
|
||||
<input type="text" name="base"></input>
|
||||
@@ -80,7 +90,7 @@ func htmlCreate(w http.ResponseWriter, baseHREF string) {
|
||||
|
||||
func htmlSearch(w http.ResponseWriter) {
|
||||
fmt.Fprintf(w, `
|
||||
<form action=%q method="post" style="padding-top: 2.5em">
|
||||
<form action=%q method="post" >
|
||||
<input type="text" name="keywords"></input>
|
||||
<button type="submit">Search</button>
|
||||
</form>
|
||||
|
||||
@@ -3,49 +3,53 @@ package server
|
||||
import (
|
||||
"fmt"
|
||||
"local/gziphttp"
|
||||
"local/notes-server/config"
|
||||
"local/router"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func (s *Server) Routes() error {
|
||||
wildcard := router.Wildcard
|
||||
endpoints := []struct {
|
||||
path string
|
||||
endpoints := map[string]struct {
|
||||
handler http.HandlerFunc
|
||||
}{
|
||||
{
|
||||
path: "/",
|
||||
"/": {
|
||||
handler: s.root,
|
||||
},
|
||||
{
|
||||
path: fmt.Sprintf("notes/%s%s", wildcard, wildcard),
|
||||
fmt.Sprintf("notes/%s%s", wildcard, wildcard): {
|
||||
handler: s.gzip(s.authenticate(s.notes)),
|
||||
},
|
||||
{
|
||||
path: fmt.Sprintf("edit/%s%s", wildcard, wildcard),
|
||||
fmt.Sprintf("edit/%s%s", wildcard, wildcard): {
|
||||
handler: s.gzip(s.authenticate(s.edit)),
|
||||
},
|
||||
{
|
||||
path: fmt.Sprintf("delete/%s%s", wildcard, wildcard),
|
||||
fmt.Sprintf("delete/%s%s", wildcard, wildcard): {
|
||||
handler: s.gzip(s.authenticate(s.delete)),
|
||||
},
|
||||
{
|
||||
path: fmt.Sprintf("submit/%s%s", wildcard, wildcard),
|
||||
fmt.Sprintf("submit/%s%s", wildcard, wildcard): {
|
||||
handler: s.gzip(s.authenticate(s.submit)),
|
||||
},
|
||||
{
|
||||
path: fmt.Sprintf("create/%s%s", wildcard, wildcard),
|
||||
fmt.Sprintf("create/%s%s", wildcard, wildcard): {
|
||||
handler: s.gzip(s.authenticate(s.create)),
|
||||
},
|
||||
{
|
||||
path: fmt.Sprintf("search"),
|
||||
fmt.Sprintf("comment/%s%s", wildcard, wildcard): {
|
||||
handler: s.gzip(s.authenticate(s.comment)),
|
||||
},
|
||||
fmt.Sprintf("search"): {
|
||||
handler: s.gzip(s.authenticate(s.search)),
|
||||
},
|
||||
}
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
if err := s.Add(endpoint.path, endpoint.handler); err != nil {
|
||||
for path, endpoint := range endpoints {
|
||||
if config.ReadOnly {
|
||||
for _, prefix := range []string{"edit/", "delete/", "delete/", "create/", "submit/"} {
|
||||
if strings.HasPrefix(path, prefix) {
|
||||
endpoint.handler = http.NotFound
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := s.Add(path, endpoint.handler); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ func (s *Server) search(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
head(w, r)
|
||||
fmt.Fprintln(w, h2(filetree.NewPathFromURL("/notes").MultiLink()))
|
||||
htmlSearch(w)
|
||||
fmt.Fprintln(w, h1(keywords), results)
|
||||
foot(w, r)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user