Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,15 @@ It's a basic file format. Every command must be on it's own line empty lines are
| `Page` | Creates a new page |
| `--` | Inserts a horizontal rule and resets columns |

## Editing

The `/edit` page allows updating the entire bookmark file.
Each category heading on the index page now includes a small pencil icon
link that opens `/editCategory`. This page shows only the selected
category text and saves changes back to your bookmarks without touching
other sections. Edits check the file's SHA so you'll get an error if it
changed while you were editing.

![img.png](media/img.png)

![img_1.png](media/img_1.png)
Expand Down
22 changes: 14 additions & 8 deletions bookmarkAccess.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ func GetBookmarksRepoName() string {
return "MyBookmarks"
}

func UpdateBookmarks(ctx context.Context, githubUser string, userToken *oauth2.Token, sourceRef, branch, text string) error {
func UpdateBookmarks(ctx context.Context, githubUser string, userToken *oauth2.Token, sourceRef, branch, text, expectSHA string) error {
client := github.NewClient(oauth2.NewClient(ctx, oauth2.StaticTokenSource(userToken)))
defaultBranch, created, err := GetDefaultBranch(ctx, githubUser, client, branch)
if err != nil {
Expand Down Expand Up @@ -62,14 +62,16 @@ func UpdateBookmarks(ctx context.Context, githubUser string, userToken *oauth2.T
return fmt.Errorf("CreateRepo: %w", err)
}
return CreateBookmarks(ctx, githubUser, userToken, branch, text)
err = nil
}
if err != nil {
return fmt.Errorf("UpdateBookmarks: client.Repositories.GetContents: %w", err)
}
if contents == nil || contents.Content == nil {
return nil
}
if expectSHA != "" && contents.SHA != nil && *contents.SHA != expectSHA {
return fmt.Errorf("bookmarks modified concurrently")
}
_, _, err = client.Repositories.UpdateFile(ctx, githubUser, RepoName, "bookmarks.txt", &github.RepositoryContentFileOptions{
Message: SP("Auto change from web"),
Content: []byte(text),
Expand Down Expand Up @@ -182,24 +184,28 @@ func CreateBookmarks(ctx context.Context, githubUser string, userToken *oauth2.T
return nil
}

func GetBookmarks(ctx context.Context, githubUser string, ref string, userToken *oauth2.Token) (string, error) {
func GetBookmarks(ctx context.Context, githubUser string, ref string, userToken *oauth2.Token) (string, string, error) {
client := github.NewClient(oauth2.NewClient(ctx, oauth2.StaticTokenSource(userToken)))
contents, _, resp, err := client.Repositories.GetContents(ctx, githubUser, RepoName, "bookmarks.txt", &github.RepositoryContentGetOptions{
Ref: ref,
})
switch resp.StatusCode {
case http.StatusNotFound:
return "", nil
return "", "", nil
}
if err != nil {
return "", fmt.Errorf("GetBookmarks: %w", err)
return "", "", fmt.Errorf("GetBookmarks: %w", err)
}
if contents.Content == nil {
return "", nil
return "", "", nil
}
s, err := base64.StdEncoding.DecodeString(*contents.Content)
if err != nil {
return "", fmt.Errorf("GetBookmarks: StdEncoding.DecodeString: %w", err)
return "", "", fmt.Errorf("GetBookmarks: StdEncoding.DecodeString: %w", err)
}
sha := ""
if contents.SHA != nil {
sha = *contents.SHA
}
return string(s), nil
return string(s), sha, nil
}
49 changes: 48 additions & 1 deletion bookmarkActionHandlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"github.com/gorilla/sessions"
"golang.org/x/oauth2"
"net/http"
"strconv"
)

func BookmarksEditSaveAction(w http.ResponseWriter, r *http.Request) error {
Expand All @@ -15,13 +16,22 @@ func BookmarksEditSaveAction(w http.ResponseWriter, r *http.Request) error {
token, _ := session.Values["Token"].(*oauth2.Token)
branch := r.PostFormValue("branch")
ref := r.PostFormValue("ref")
sha := r.PostFormValue("sha")

login := ""
if githubUser != nil && githubUser.Login != nil {
login = *githubUser.Login
}

if err := UpdateBookmarks(r.Context(), login, token, ref, branch, text); err != nil {
_, curSha, err := GetBookmarks(r.Context(), login, ref, token)
if err != nil {
return fmt.Errorf("GetBookmarks: %w", err)
}
if sha != "" && curSha != sha {
return fmt.Errorf("bookmark modified concurrently")
}

if err := UpdateBookmarks(r.Context(), login, token, ref, branch, text, curSha); err != nil {
return fmt.Errorf("updateBookmark error: %w", err)
}
return nil
Expand All @@ -44,3 +54,40 @@ func BookmarksEditCreateAction(w http.ResponseWriter, r *http.Request) error {
}
return nil
}

func CategoryEditSaveAction(w http.ResponseWriter, r *http.Request) error {
text := r.PostFormValue("text")
idxStr := r.URL.Query().Get("index")
idx, err := strconv.Atoi(idxStr)
if err != nil {
return fmt.Errorf("invalid index: %w", err)
}
session := r.Context().Value(ContextValues("session")).(*sessions.Session)
githubUser, _ := session.Values["GithubUser"].(*github.User)
token, _ := session.Values["Token"].(*oauth2.Token)
branch := r.PostFormValue("branch")
ref := r.PostFormValue("ref")
sha := r.PostFormValue("sha")

login := ""
if githubUser != nil && githubUser.Login != nil {
login = *githubUser.Login
}

currentBookmarks, curSha, err := GetBookmarks(r.Context(), login, ref, token)
if err != nil {
return fmt.Errorf("GetBookmarks: %w", err)
}
if sha != "" && curSha != sha {
return fmt.Errorf("bookmark modified concurrently")
}
updated, err := ReplaceCategoryByIndex(currentBookmarks, idx, text)
if err != nil {
return fmt.Errorf("ReplaceCategory: %w", err)
}

if err := UpdateBookmarks(r.Context(), login, token, ref, branch, updated, curSha); err != nil {
return fmt.Errorf("updateBookmark error: %w", err)
}
return nil
}
77 changes: 77 additions & 0 deletions bookmarkCategoryEdit.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package gobookmarks

import (
"fmt"
"strings"
)

// ExtractCategoryByIndex returns the category text for the nth category (0 based)
func ExtractCategoryByIndex(bookmarks string, index int) (string, error) {
lines := strings.Split(bookmarks, "\n")
currentIndex := -1
start := -1
end := -1
for i, line := range lines {
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(strings.ToLower(trimmed), "category:") {
currentIndex++
if currentIndex == index {
start = i
for j := i + 1; j <= len(lines); j++ {
if j == len(lines) {
end = j
break
}
t := strings.TrimSpace(lines[j])
if strings.HasPrefix(strings.ToLower(t), "category:") || strings.EqualFold(t, "column") || strings.EqualFold(t, "page") || t == "--" {
end = j
break
}
}
break
}
}
}
if start == -1 || end == -1 {
return "", fmt.Errorf("category index %d not found", index)
}
return strings.Join(lines[start:end], "\n"), nil
}

// ReplaceCategoryByIndex replaces the nth category with newText
func ReplaceCategoryByIndex(bookmarks string, index int, newText string) (string, error) {
lines := strings.Split(bookmarks, "\n")
currentIndex := -1
start := -1
end := -1
for i, line := range lines {
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(strings.ToLower(trimmed), "category:") {
currentIndex++
if currentIndex == index {
start = i
for j := i + 1; j <= len(lines); j++ {
if j == len(lines) {
end = j
break
}
t := strings.TrimSpace(lines[j])
if strings.HasPrefix(strings.ToLower(t), "category:") || strings.EqualFold(t, "column") || strings.EqualFold(t, "page") || t == "--" {
end = j
break
}
}
break
}
}
}
if start == -1 || end == -1 {
return "", fmt.Errorf("category index %d not found", index)
}
var result []string
result = append(result, lines[:start]...)
newLines := strings.Split(newText, "\n")
result = append(result, newLines...)
result = append(result, lines[end:]...)
return strings.Join(result, "\n"), nil
}
68 changes: 68 additions & 0 deletions bookmarkCategoryEdit_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package gobookmarks

import "testing"

const testBookmarkText = `Category: A
http://a.com a
Column
Category: B
http://b.com b
`

func TestExtractCategoryByIndex(t *testing.T) {
got, err := ExtractCategoryByIndex(testBookmarkText, 1)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
expected := "Category: B\nhttp://b.com b\n"
if got != expected {
t.Fatalf("expected %q got %q", expected, got)
}
}

func TestExtractCategoryByIndexFirst(t *testing.T) {
got, err := ExtractCategoryByIndex(testBookmarkText, 0)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
expected := "Category: A\nhttp://a.com a"
if got != expected {
t.Fatalf("expected %q got %q", expected, got)
}
}

func TestExtractCategoryByIndexError(t *testing.T) {
if _, err := ExtractCategoryByIndex(testBookmarkText, 5); err == nil {
t.Fatalf("expected error")
}
}

func TestReplaceCategoryByIndex(t *testing.T) {
newSection := "Category: B\nhttp://new.com n"
updated, err := ReplaceCategoryByIndex(testBookmarkText, 1, newSection)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
expected := "Category: A\nhttp://a.com a\nColumn\n" + newSection
if updated != expected {
t.Fatalf("expected %q got %q", expected, updated)
}
}

func TestReplaceCategoryByIndexFirst(t *testing.T) {
newSection := "Category: A\nhttp://changed.com x"
updated, err := ReplaceCategoryByIndex(testBookmarkText, 0, newSection)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
expected := newSection + "\nColumn\nCategory: B\nhttp://b.com b\n"
if updated != expected {
t.Fatalf("expected %q got %q", expected, updated)
}
}

func TestReplaceCategoryByIndexError(t *testing.T) {
if _, err := ReplaceCategoryByIndex(testBookmarkText, 3, "foo"); err == nil {
t.Fatalf("expected error")
}
}
12 changes: 12 additions & 0 deletions bookmarkProcessor.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ type BookmarkEntry struct {
type BookmarkCategory struct {
Name string
Entries []*BookmarkEntry
Index int
}

type BookmarkColumn struct {
Expand All @@ -31,11 +32,14 @@ func PreprocessBookmarks(bookmarks string) []*BookmarkPage {
lines := strings.Split(bookmarks, "\n")
var result = []*BookmarkPage{{Blocks: []*BookmarkBlock{{Columns: []*BookmarkColumn{{}}}}}}
var currentCategory *BookmarkCategory
idx := 0

for _, line := range lines {
line = strings.TrimSpace(line)
if strings.EqualFold(line, "Page") {
if currentCategory != nil {
currentCategory.Index = idx
idx++
lastBlock := result[len(result)-1].Blocks[len(result[len(result)-1].Blocks)-1]
lastColumn := lastBlock.Columns[len(lastBlock.Columns)-1]
lastColumn.Categories = append(lastColumn.Categories, currentCategory)
Expand All @@ -46,6 +50,8 @@ func PreprocessBookmarks(bookmarks string) []*BookmarkPage {
}
if line == "--" {
if currentCategory != nil {
currentCategory.Index = idx
idx++
lastBlock := result[len(result)-1].Blocks[len(result[len(result)-1].Blocks)-1]
lastColumn := lastBlock.Columns[len(lastBlock.Columns)-1]
lastColumn.Categories = append(lastColumn.Categories, currentCategory)
Expand All @@ -58,6 +64,8 @@ func PreprocessBookmarks(bookmarks string) []*BookmarkPage {
}
if strings.EqualFold(line, "column") {
if currentCategory != nil {
currentCategory.Index = idx
idx++
lastBlock := result[len(result)-1].Blocks[len(result[len(result)-1].Blocks)-1]
lastColumn := lastBlock.Columns[len(lastBlock.Columns)-1]
lastColumn.Categories = append(lastColumn.Categories, currentCategory)
Expand All @@ -76,6 +84,8 @@ func PreprocessBookmarks(bookmarks string) []*BookmarkPage {
if currentCategory == nil {
currentCategory = &BookmarkCategory{Name: categoryName}
} else if currentCategory.Name != "" {
currentCategory.Index = idx
idx++
lastBlock := result[len(result)-1].Blocks[len(result[len(result)-1].Blocks)-1]
lastColumn := lastBlock.Columns[len(lastBlock.Columns)-1]
lastColumn.Categories = append(lastColumn.Categories, currentCategory)
Expand All @@ -95,6 +105,8 @@ func PreprocessBookmarks(bookmarks string) []*BookmarkPage {
}

if currentCategory != nil && currentCategory.Name != "" {
currentCategory.Index = idx
idx++
lastBlock := result[len(result)-1].Blocks[len(result[len(result)-1].Blocks)-1]
lastColumn := lastBlock.Columns[len(lastBlock.Columns)-1]
lastColumn.Categories = append(lastColumn.Categories, currentCategory)
Expand Down
Loading
Loading