Skip to content

Commit 5dc24da

Browse files
authored
Merge pull request #17 from arran4/codex/add-category-specific-edit-functionality
Add category edit feature
2 parents 2723a13 + d5ce565 commit 5dc24da

14 files changed

+364
-17
lines changed

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,15 @@ It's a basic file format. Every command must be on it's own line empty lines are
4444
| `Page` | Creates a new page |
4545
| `--` | Inserts a horizontal rule and resets columns |
4646

47+
## Editing
48+
49+
The `/edit` page allows updating the entire bookmark file.
50+
Each category heading on the index page now includes a small pencil icon
51+
link that opens `/editCategory`. This page shows only the selected
52+
category text and saves changes back to your bookmarks without touching
53+
other sections. Edits check the file's SHA so you'll get an error if it
54+
changed while you were editing.
55+
4756
![img.png](media/img.png)
4857

4958
![img_1.png](media/img_1.png)

bookmarkAccess.go

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ func GetBookmarksRepoName() string {
2626
return "MyBookmarks"
2727
}
2828

29-
func UpdateBookmarks(ctx context.Context, githubUser string, userToken *oauth2.Token, sourceRef, branch, text string) error {
29+
func UpdateBookmarks(ctx context.Context, githubUser string, userToken *oauth2.Token, sourceRef, branch, text, expectSHA string) error {
3030
client := github.NewClient(oauth2.NewClient(ctx, oauth2.StaticTokenSource(userToken)))
3131
defaultBranch, created, err := GetDefaultBranch(ctx, githubUser, client, branch)
3232
if err != nil {
@@ -62,14 +62,16 @@ func UpdateBookmarks(ctx context.Context, githubUser string, userToken *oauth2.T
6262
return fmt.Errorf("CreateRepo: %w", err)
6363
}
6464
return CreateBookmarks(ctx, githubUser, userToken, branch, text)
65-
err = nil
6665
}
6766
if err != nil {
6867
return fmt.Errorf("UpdateBookmarks: client.Repositories.GetContents: %w", err)
6968
}
7069
if contents == nil || contents.Content == nil {
7170
return nil
7271
}
72+
if expectSHA != "" && contents.SHA != nil && *contents.SHA != expectSHA {
73+
return fmt.Errorf("bookmarks modified concurrently")
74+
}
7375
_, _, err = client.Repositories.UpdateFile(ctx, githubUser, RepoName, "bookmarks.txt", &github.RepositoryContentFileOptions{
7476
Message: SP("Auto change from web"),
7577
Content: []byte(text),
@@ -182,24 +184,28 @@ func CreateBookmarks(ctx context.Context, githubUser string, userToken *oauth2.T
182184
return nil
183185
}
184186

185-
func GetBookmarks(ctx context.Context, githubUser string, ref string, userToken *oauth2.Token) (string, error) {
187+
func GetBookmarks(ctx context.Context, githubUser string, ref string, userToken *oauth2.Token) (string, string, error) {
186188
client := github.NewClient(oauth2.NewClient(ctx, oauth2.StaticTokenSource(userToken)))
187189
contents, _, resp, err := client.Repositories.GetContents(ctx, githubUser, RepoName, "bookmarks.txt", &github.RepositoryContentGetOptions{
188190
Ref: ref,
189191
})
190192
switch resp.StatusCode {
191193
case http.StatusNotFound:
192-
return "", nil
194+
return "", "", nil
193195
}
194196
if err != nil {
195-
return "", fmt.Errorf("GetBookmarks: %w", err)
197+
return "", "", fmt.Errorf("GetBookmarks: %w", err)
196198
}
197199
if contents.Content == nil {
198-
return "", nil
200+
return "", "", nil
199201
}
200202
s, err := base64.StdEncoding.DecodeString(*contents.Content)
201203
if err != nil {
202-
return "", fmt.Errorf("GetBookmarks: StdEncoding.DecodeString: %w", err)
204+
return "", "", fmt.Errorf("GetBookmarks: StdEncoding.DecodeString: %w", err)
205+
}
206+
sha := ""
207+
if contents.SHA != nil {
208+
sha = *contents.SHA
203209
}
204-
return string(s), nil
210+
return string(s), sha, nil
205211
}

bookmarkActionHandlers.go

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"github.com/gorilla/sessions"
77
"golang.org/x/oauth2"
88
"net/http"
9+
"strconv"
910
)
1011

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

1921
login := ""
2022
if githubUser != nil && githubUser.Login != nil {
2123
login = *githubUser.Login
2224
}
2325

24-
if err := UpdateBookmarks(r.Context(), login, token, ref, branch, text); err != nil {
26+
_, curSha, err := GetBookmarks(r.Context(), login, ref, token)
27+
if err != nil {
28+
return fmt.Errorf("GetBookmarks: %w", err)
29+
}
30+
if sha != "" && curSha != sha {
31+
return fmt.Errorf("bookmark modified concurrently")
32+
}
33+
34+
if err := UpdateBookmarks(r.Context(), login, token, ref, branch, text, curSha); err != nil {
2535
return fmt.Errorf("updateBookmark error: %w", err)
2636
}
2737
return nil
@@ -44,3 +54,40 @@ func BookmarksEditCreateAction(w http.ResponseWriter, r *http.Request) error {
4454
}
4555
return nil
4656
}
57+
58+
func CategoryEditSaveAction(w http.ResponseWriter, r *http.Request) error {
59+
text := r.PostFormValue("text")
60+
idxStr := r.URL.Query().Get("index")
61+
idx, err := strconv.Atoi(idxStr)
62+
if err != nil {
63+
return fmt.Errorf("invalid index: %w", err)
64+
}
65+
session := r.Context().Value(ContextValues("session")).(*sessions.Session)
66+
githubUser, _ := session.Values["GithubUser"].(*github.User)
67+
token, _ := session.Values["Token"].(*oauth2.Token)
68+
branch := r.PostFormValue("branch")
69+
ref := r.PostFormValue("ref")
70+
sha := r.PostFormValue("sha")
71+
72+
login := ""
73+
if githubUser != nil && githubUser.Login != nil {
74+
login = *githubUser.Login
75+
}
76+
77+
currentBookmarks, curSha, err := GetBookmarks(r.Context(), login, ref, token)
78+
if err != nil {
79+
return fmt.Errorf("GetBookmarks: %w", err)
80+
}
81+
if sha != "" && curSha != sha {
82+
return fmt.Errorf("bookmark modified concurrently")
83+
}
84+
updated, err := ReplaceCategoryByIndex(currentBookmarks, idx, text)
85+
if err != nil {
86+
return fmt.Errorf("ReplaceCategory: %w", err)
87+
}
88+
89+
if err := UpdateBookmarks(r.Context(), login, token, ref, branch, updated, curSha); err != nil {
90+
return fmt.Errorf("updateBookmark error: %w", err)
91+
}
92+
return nil
93+
}

bookmarkCategoryEdit.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package gobookmarks
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
)
7+
8+
// ExtractCategoryByIndex returns the category text for the nth category (0 based)
9+
func ExtractCategoryByIndex(bookmarks string, index int) (string, error) {
10+
lines := strings.Split(bookmarks, "\n")
11+
currentIndex := -1
12+
start := -1
13+
end := -1
14+
for i, line := range lines {
15+
trimmed := strings.TrimSpace(line)
16+
if strings.HasPrefix(strings.ToLower(trimmed), "category:") {
17+
currentIndex++
18+
if currentIndex == index {
19+
start = i
20+
for j := i + 1; j <= len(lines); j++ {
21+
if j == len(lines) {
22+
end = j
23+
break
24+
}
25+
t := strings.TrimSpace(lines[j])
26+
if strings.HasPrefix(strings.ToLower(t), "category:") || strings.EqualFold(t, "column") || strings.EqualFold(t, "page") || t == "--" {
27+
end = j
28+
break
29+
}
30+
}
31+
break
32+
}
33+
}
34+
}
35+
if start == -1 || end == -1 {
36+
return "", fmt.Errorf("category index %d not found", index)
37+
}
38+
return strings.Join(lines[start:end], "\n"), nil
39+
}
40+
41+
// ReplaceCategoryByIndex replaces the nth category with newText
42+
func ReplaceCategoryByIndex(bookmarks string, index int, newText string) (string, error) {
43+
lines := strings.Split(bookmarks, "\n")
44+
currentIndex := -1
45+
start := -1
46+
end := -1
47+
for i, line := range lines {
48+
trimmed := strings.TrimSpace(line)
49+
if strings.HasPrefix(strings.ToLower(trimmed), "category:") {
50+
currentIndex++
51+
if currentIndex == index {
52+
start = i
53+
for j := i + 1; j <= len(lines); j++ {
54+
if j == len(lines) {
55+
end = j
56+
break
57+
}
58+
t := strings.TrimSpace(lines[j])
59+
if strings.HasPrefix(strings.ToLower(t), "category:") || strings.EqualFold(t, "column") || strings.EqualFold(t, "page") || t == "--" {
60+
end = j
61+
break
62+
}
63+
}
64+
break
65+
}
66+
}
67+
}
68+
if start == -1 || end == -1 {
69+
return "", fmt.Errorf("category index %d not found", index)
70+
}
71+
var result []string
72+
result = append(result, lines[:start]...)
73+
newLines := strings.Split(newText, "\n")
74+
result = append(result, newLines...)
75+
result = append(result, lines[end:]...)
76+
return strings.Join(result, "\n"), nil
77+
}

bookmarkCategoryEdit_test.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package gobookmarks
2+
3+
import "testing"
4+
5+
const testBookmarkText = `Category: A
6+
http://a.com a
7+
Column
8+
Category: B
9+
http://b.com b
10+
`
11+
12+
func TestExtractCategoryByIndex(t *testing.T) {
13+
got, err := ExtractCategoryByIndex(testBookmarkText, 1)
14+
if err != nil {
15+
t.Fatalf("unexpected error: %v", err)
16+
}
17+
expected := "Category: B\nhttp://b.com b\n"
18+
if got != expected {
19+
t.Fatalf("expected %q got %q", expected, got)
20+
}
21+
}
22+
23+
func TestExtractCategoryByIndexFirst(t *testing.T) {
24+
got, err := ExtractCategoryByIndex(testBookmarkText, 0)
25+
if err != nil {
26+
t.Fatalf("unexpected error: %v", err)
27+
}
28+
expected := "Category: A\nhttp://a.com a"
29+
if got != expected {
30+
t.Fatalf("expected %q got %q", expected, got)
31+
}
32+
}
33+
34+
func TestExtractCategoryByIndexError(t *testing.T) {
35+
if _, err := ExtractCategoryByIndex(testBookmarkText, 5); err == nil {
36+
t.Fatalf("expected error")
37+
}
38+
}
39+
40+
func TestReplaceCategoryByIndex(t *testing.T) {
41+
newSection := "Category: B\nhttp://new.com n"
42+
updated, err := ReplaceCategoryByIndex(testBookmarkText, 1, newSection)
43+
if err != nil {
44+
t.Fatalf("unexpected error: %v", err)
45+
}
46+
expected := "Category: A\nhttp://a.com a\nColumn\n" + newSection
47+
if updated != expected {
48+
t.Fatalf("expected %q got %q", expected, updated)
49+
}
50+
}
51+
52+
func TestReplaceCategoryByIndexFirst(t *testing.T) {
53+
newSection := "Category: A\nhttp://changed.com x"
54+
updated, err := ReplaceCategoryByIndex(testBookmarkText, 0, newSection)
55+
if err != nil {
56+
t.Fatalf("unexpected error: %v", err)
57+
}
58+
expected := newSection + "\nColumn\nCategory: B\nhttp://b.com b\n"
59+
if updated != expected {
60+
t.Fatalf("expected %q got %q", expected, updated)
61+
}
62+
}
63+
64+
func TestReplaceCategoryByIndexError(t *testing.T) {
65+
if _, err := ReplaceCategoryByIndex(testBookmarkText, 3, "foo"); err == nil {
66+
t.Fatalf("expected error")
67+
}
68+
}

bookmarkProcessor.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ type BookmarkEntry struct {
1212
type BookmarkCategory struct {
1313
Name string
1414
Entries []*BookmarkEntry
15+
Index int
1516
}
1617

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

3537
for _, line := range lines {
3638
line = strings.TrimSpace(line)
3739
if strings.EqualFold(line, "Page") {
3840
if currentCategory != nil {
41+
currentCategory.Index = idx
42+
idx++
3943
lastBlock := result[len(result)-1].Blocks[len(result[len(result)-1].Blocks)-1]
4044
lastColumn := lastBlock.Columns[len(lastBlock.Columns)-1]
4145
lastColumn.Categories = append(lastColumn.Categories, currentCategory)
@@ -46,6 +50,8 @@ func PreprocessBookmarks(bookmarks string) []*BookmarkPage {
4650
}
4751
if line == "--" {
4852
if currentCategory != nil {
53+
currentCategory.Index = idx
54+
idx++
4955
lastBlock := result[len(result)-1].Blocks[len(result[len(result)-1].Blocks)-1]
5056
lastColumn := lastBlock.Columns[len(lastBlock.Columns)-1]
5157
lastColumn.Categories = append(lastColumn.Categories, currentCategory)
@@ -58,6 +64,8 @@ func PreprocessBookmarks(bookmarks string) []*BookmarkPage {
5864
}
5965
if strings.EqualFold(line, "column") {
6066
if currentCategory != nil {
67+
currentCategory.Index = idx
68+
idx++
6169
lastBlock := result[len(result)-1].Blocks[len(result[len(result)-1].Blocks)-1]
6270
lastColumn := lastBlock.Columns[len(lastBlock.Columns)-1]
6371
lastColumn.Categories = append(lastColumn.Categories, currentCategory)
@@ -76,6 +84,8 @@ func PreprocessBookmarks(bookmarks string) []*BookmarkPage {
7684
if currentCategory == nil {
7785
currentCategory = &BookmarkCategory{Name: categoryName}
7886
} else if currentCategory.Name != "" {
87+
currentCategory.Index = idx
88+
idx++
7989
lastBlock := result[len(result)-1].Blocks[len(result[len(result)-1].Blocks)-1]
8090
lastColumn := lastBlock.Columns[len(lastBlock.Columns)-1]
8191
lastColumn.Categories = append(lastColumn.Categories, currentCategory)
@@ -95,6 +105,8 @@ func PreprocessBookmarks(bookmarks string) []*BookmarkPage {
95105
}
96106

97107
if currentCategory != nil && currentCategory.Name != "" {
108+
currentCategory.Index = idx
109+
idx++
98110
lastBlock := result[len(result)-1].Blocks[len(result[len(result)-1].Blocks)-1]
99111
lastColumn := lastBlock.Columns[len(lastBlock.Columns)-1]
100112
lastColumn.Categories = append(lastColumn.Categories, currentCategory)

0 commit comments

Comments
 (0)