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
|
package notes
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/base64"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"local/notes-server/filetree"
|
"local/notes-server/filetree"
|
||||||
|
"log"
|
||||||
|
"path"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/gomarkdown/markdown"
|
"github.com/gomarkdown/markdown"
|
||||||
|
"github.com/gomarkdown/markdown/ast"
|
||||||
"github.com/gomarkdown/markdown/html"
|
"github.com/gomarkdown/markdown/html"
|
||||||
"github.com/gomarkdown/markdown/parser"
|
"github.com/gomarkdown/markdown/parser"
|
||||||
)
|
)
|
||||||
@@ -18,8 +27,80 @@ func (n *Notes) File(urlPath string) (string, error) {
|
|||||||
b, _ := ioutil.ReadFile(p.Local)
|
b, _ := ioutil.ReadFile(p.Local)
|
||||||
renderer := html.NewRenderer(html.RendererOptions{
|
renderer := html.NewRenderer(html.RendererOptions{
|
||||||
Flags: html.CommonFlags | html.TOC,
|
Flags: html.CommonFlags | html.TOC,
|
||||||
|
RenderNodeHook: n.commentFormer(urlPath, b),
|
||||||
})
|
})
|
||||||
parser := parser.NewWithExtensions(parser.CommonExtensions | parser.HeadingIDs | parser.AutoHeadingIDs | parser.Titleblock)
|
parser := parser.NewWithExtensions(parser.CommonExtensions | parser.HeadingIDs | parser.AutoHeadingIDs | parser.Titleblock)
|
||||||
content := markdown.ToHTML(b, parser, renderer)
|
content := markdown.ToHTML(b, parser, renderer)
|
||||||
return string(content) + "\n", nil
|
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 (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"local/notes-server/config"
|
||||||
"local/notes-server/filetree"
|
"local/notes-server/filetree"
|
||||||
"net/http"
|
"net/http"
|
||||||
"path"
|
"path"
|
||||||
@@ -58,18 +59,27 @@ func fileHead(w http.ResponseWriter, baseHREF string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func htmlEdit(w http.ResponseWriter, baseHREF string) {
|
func htmlEdit(w http.ResponseWriter, baseHREF string) {
|
||||||
|
if config.ReadOnly {
|
||||||
|
return
|
||||||
|
}
|
||||||
fmt.Fprintf(w, `<div style='display:inline-block'>
|
fmt.Fprintf(w, `<div style='display:inline-block'>
|
||||||
<a href=%q><input type="button" value="Edit"></input></a>
|
<a href=%q><input type="button" value="Edit"></input></a>
|
||||||
</div><br>`, path.Join("/edit/", baseHREF))
|
</div><br>`, path.Join("/edit/", baseHREF))
|
||||||
}
|
}
|
||||||
|
|
||||||
func htmlDelete(w http.ResponseWriter, baseHREF string) {
|
func htmlDelete(w http.ResponseWriter, baseHREF string) {
|
||||||
|
if config.ReadOnly {
|
||||||
|
return
|
||||||
|
}
|
||||||
fmt.Fprintf(w, `<div style='display:inline-block'>
|
fmt.Fprintf(w, `<div style='display:inline-block'>
|
||||||
<a href=%q><input type="button" value="Delete" onclick="return confirm('Delete?');"></input></a>
|
<a href=%q><input type="button" value="Delete" onclick="return confirm('Delete?');"></input></a>
|
||||||
</div><br>`, path.Join("/delete/", baseHREF))
|
</div><br>`, path.Join("/delete/", baseHREF))
|
||||||
}
|
}
|
||||||
|
|
||||||
func htmlCreate(w http.ResponseWriter, baseHREF string) {
|
func htmlCreate(w http.ResponseWriter, baseHREF string) {
|
||||||
|
if config.ReadOnly {
|
||||||
|
return
|
||||||
|
}
|
||||||
fmt.Fprintf(w, `
|
fmt.Fprintf(w, `
|
||||||
<form action=%q method="get">
|
<form action=%q method="get">
|
||||||
<input type="text" name="base"></input>
|
<input type="text" name="base"></input>
|
||||||
@@ -80,7 +90,7 @@ func htmlCreate(w http.ResponseWriter, baseHREF string) {
|
|||||||
|
|
||||||
func htmlSearch(w http.ResponseWriter) {
|
func htmlSearch(w http.ResponseWriter) {
|
||||||
fmt.Fprintf(w, `
|
fmt.Fprintf(w, `
|
||||||
<form action=%q method="post" style="padding-top: 2.5em">
|
<form action=%q method="post" >
|
||||||
<input type="text" name="keywords"></input>
|
<input type="text" name="keywords"></input>
|
||||||
<button type="submit">Search</button>
|
<button type="submit">Search</button>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -3,49 +3,53 @@ package server
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"local/gziphttp"
|
"local/gziphttp"
|
||||||
|
"local/notes-server/config"
|
||||||
"local/router"
|
"local/router"
|
||||||
"net/http"
|
"net/http"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *Server) Routes() error {
|
func (s *Server) Routes() error {
|
||||||
wildcard := router.Wildcard
|
wildcard := router.Wildcard
|
||||||
endpoints := []struct {
|
endpoints := map[string]struct {
|
||||||
path string
|
|
||||||
handler http.HandlerFunc
|
handler http.HandlerFunc
|
||||||
}{
|
}{
|
||||||
{
|
"/": {
|
||||||
path: "/",
|
|
||||||
handler: s.root,
|
handler: s.root,
|
||||||
},
|
},
|
||||||
{
|
fmt.Sprintf("notes/%s%s", wildcard, wildcard): {
|
||||||
path: fmt.Sprintf("notes/%s%s", wildcard, wildcard),
|
|
||||||
handler: s.gzip(s.authenticate(s.notes)),
|
handler: s.gzip(s.authenticate(s.notes)),
|
||||||
},
|
},
|
||||||
{
|
fmt.Sprintf("edit/%s%s", wildcard, wildcard): {
|
||||||
path: fmt.Sprintf("edit/%s%s", wildcard, wildcard),
|
|
||||||
handler: s.gzip(s.authenticate(s.edit)),
|
handler: s.gzip(s.authenticate(s.edit)),
|
||||||
},
|
},
|
||||||
{
|
fmt.Sprintf("delete/%s%s", wildcard, wildcard): {
|
||||||
path: fmt.Sprintf("delete/%s%s", wildcard, wildcard),
|
|
||||||
handler: s.gzip(s.authenticate(s.delete)),
|
handler: s.gzip(s.authenticate(s.delete)),
|
||||||
},
|
},
|
||||||
{
|
fmt.Sprintf("submit/%s%s", wildcard, wildcard): {
|
||||||
path: fmt.Sprintf("submit/%s%s", wildcard, wildcard),
|
|
||||||
handler: s.gzip(s.authenticate(s.submit)),
|
handler: s.gzip(s.authenticate(s.submit)),
|
||||||
},
|
},
|
||||||
{
|
fmt.Sprintf("create/%s%s", wildcard, wildcard): {
|
||||||
path: fmt.Sprintf("create/%s%s", wildcard, wildcard),
|
|
||||||
handler: s.gzip(s.authenticate(s.create)),
|
handler: s.gzip(s.authenticate(s.create)),
|
||||||
},
|
},
|
||||||
{
|
fmt.Sprintf("comment/%s%s", wildcard, wildcard): {
|
||||||
path: fmt.Sprintf("search"),
|
handler: s.gzip(s.authenticate(s.comment)),
|
||||||
|
},
|
||||||
|
fmt.Sprintf("search"): {
|
||||||
handler: s.gzip(s.authenticate(s.search)),
|
handler: s.gzip(s.authenticate(s.search)),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, endpoint := range endpoints {
|
for path, endpoint := range endpoints {
|
||||||
if err := s.Add(endpoint.path, endpoint.handler); err != nil {
|
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
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ func (s *Server) search(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
head(w, r)
|
head(w, r)
|
||||||
fmt.Fprintln(w, h2(filetree.NewPathFromURL("/notes").MultiLink()))
|
fmt.Fprintln(w, h2(filetree.NewPathFromURL("/notes").MultiLink()))
|
||||||
|
htmlSearch(w)
|
||||||
fmt.Fprintln(w, h1(keywords), results)
|
fmt.Fprintln(w, h1(keywords), results)
|
||||||
foot(w, r)
|
foot(w, r)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user