package main import ( "bytes" "html/template" "log" "net/http" "os" "path/filepath" "strings" "time" ) var quickAddTmpl = template.Must(template.ParseFS(assets, "assets/quickadd.html")) type quickAddData struct { To, URL, Title string } // handleQuickAdd serves the bookmarklet popup at /quickadd. func (h *handler) handleQuickAdd(w http.ResponseWriter, r *http.Request) { if !h.checkAuth(w, r) { return } if r.Method != http.MethodGet { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } q := r.URL.Query() to := strings.TrimSpace(q.Get("to")) if to == "" { http.Error(w, "missing to", http.StatusBadRequest) return } if !strings.HasPrefix(to, "/") { to = "/" + to } data := quickAddData{ To: to, URL: q.Get("url"), Title: q.Get("title"), } w.Header().Set("Content-Type", "text/html; charset=utf-8") if err := quickAddTmpl.Execute(w, data); err != nil { log.Printf("quickadd template: %v", err) } } // handleAppend appends one link entry to index.md at fsPath. Creates the // folder and index.md if missing. Body is form-encoded with `url` (required), // `title` and `comment` (both optional). func (h *handler) handleAppend(w http.ResponseWriter, r *http.Request, urlPath, fsPath string) { if err := r.ParseForm(); err != nil { http.Error(w, "bad request", http.StatusBadRequest) return } rawURL := strings.TrimSpace(r.FormValue("url")) title := strings.TrimSpace(r.FormValue("title")) comment := strings.TrimSpace(r.FormValue("comment")) if rawURL == "" { http.Error(w, "missing url", http.StatusBadRequest) return } if title == "" { title = rawURL } entry := formatAppendEntry(title, rawURL, comment, time.Now()) _, statErr := os.Stat(fsPath) newlyCreated := os.IsNotExist(statErr) if err := os.MkdirAll(fsPath, 0755); err != nil { http.Error(w, "mkdir failed: "+err.Error(), http.StatusInternalServerError) return } indexPath := filepath.Join(fsPath, "index.md") existing, err := os.ReadFile(indexPath) if err != nil && !os.IsNotExist(err) { http.Error(w, "read failed: "+err.Error(), http.StatusInternalServerError) return } var buf bytes.Buffer if len(existing) > 0 { buf.Write(existing) if existing[len(existing)-1] != '\n' { buf.WriteByte('\n') } } buf.WriteString(entry) if err := os.WriteFile(indexPath, buf.Bytes(), 0644); err != nil { http.Error(w, "write failed: "+err.Error(), http.StatusInternalServerError) return } if newlyCreated { if rel, err := filepath.Rel(h.root, fsPath); err == nil { folderIndexAdd(filepath.ToSlash(rel)) } } w.WriteHeader(http.StatusNoContent) } // formatAppendEntry builds a CommonMark multi-line list item: a link with a // continuation-indented timestamp line and an optional comment line. Lines // after the first share an indent so goldmark folds them into one paragraph. func formatAppendEntry(title, rawURL, comment string, ts time.Time) string { var b strings.Builder b.WriteString("- [") b.WriteString(escapeLinkLabel(title)) b.WriteString("](") b.WriteString(rawURL) b.WriteString(")
") b.WriteString(ts.Format("2006-01-02 15:04")) b.WriteString("
") if comment != "" { b.WriteString(" ") b.WriteString(comment) b.WriteByte('\n') } return b.String() } // escapeLinkLabel backslash-escapes the brackets that would otherwise close // the markdown link label early. The label text is rendered verbatim, so we // keep all other characters as-is. func escapeLinkLabel(s string) string { s = strings.ReplaceAll(s, `\`, `\\`) s = strings.ReplaceAll(s, `[`, `\[`) s = strings.ReplaceAll(s, `]`, `\]`) return s }