diff --git a/notes/comment.go b/notes/comment.go new file mode 100755 index 0000000..5e6d88a --- /dev/null +++ b/notes/comment.go @@ -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) +} diff --git a/notes/comment_test.go b/notes/comment_test.go new file mode 100644 index 0000000..db80125 --- /dev/null +++ b/notes/comment_test.go @@ -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) + } +} diff --git a/notes/file.go b/notes/file.go index 63fc8b8..adc52e0 100755 --- a/notes/file.go +++ b/notes/file.go @@ -1,12 +1,16 @@ package notes import ( + "bytes" "encoding/base64" "errors" "fmt" "io" "io/ioutil" "local/notes-server/filetree" + "log" + "path" + "regexp" "strings" "github.com/gomarkdown/markdown" @@ -23,15 +27,57 @@ 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(), + 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() html.RenderNodeFunc { - header_n := 0 +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, ` +
+ + + +
+ `, 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 { diff --git a/server/comment.go b/server/comment.go new file mode 100755 index 0000000..e2797ba --- /dev/null +++ b/server/comment.go @@ -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) +} diff --git a/server/routes.go b/server/routes.go index 2e9a3f0..6663dd4 100755 --- a/server/routes.go +++ b/server/routes.go @@ -33,6 +33,9 @@ func (s *Server) Routes() error { fmt.Sprintf("create/%s%s", wildcard, wildcard): { handler: s.gzip(s.authenticate(s.create)), }, + fmt.Sprintf("comment/%s%s", wildcard, wildcard): { + handler: s.gzip(s.authenticate(s.comment)), + }, fmt.Sprintf("search"): { handler: s.gzip(s.authenticate(s.search)), },