Add comments per heading
parent
785215bd3c
commit
8bf7503c8e
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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,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, `
|
||||
<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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)),
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in New Issue