Skip to content

Commit 9bf5b7e

Browse files
committed
Add tab edit and creation
1 parent 07b54d7 commit 9bf5b7e

File tree

12 files changed

+379
-19
lines changed

12 files changed

+379
-19
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ It's a basic file format. Every command must be on it's own line empty lines are
4242
| `<Link> <Name>` | Will create a link to `<Link>` with the display name `<Name>` |
4343
| `Column` | Will create a column |
4444
| `Page` | Creates a new page |
45+
| `Tab: <name>` | Starts a new named tab (switch using `?tab=<name>`) |
4546
| `--` | Inserts a horizontal rule and resets columns |
4647

4748
## Editing

bookmarkProcessor.go

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,16 +26,31 @@ type BookmarkBlock struct {
2626

2727
type BookmarkPage struct {
2828
Blocks []*BookmarkBlock
29+
Tab string
2930
}
3031

3132
func PreprocessBookmarks(bookmarks string) []*BookmarkPage {
3233
lines := strings.Split(bookmarks, "\n")
3334
var result = []*BookmarkPage{{Blocks: []*BookmarkBlock{{Columns: []*BookmarkColumn{{}}}}}}
3435
var currentCategory *BookmarkCategory
36+
currentTab := ""
3537
idx := 0
3638

3739
for _, line := range lines {
3840
line = strings.TrimSpace(line)
41+
if strings.HasPrefix(strings.ToLower(line), "tab:") {
42+
if currentCategory != nil {
43+
currentCategory.Index = idx
44+
idx++
45+
lastBlock := result[len(result)-1].Blocks[len(result[len(result)-1].Blocks)-1]
46+
lastColumn := lastBlock.Columns[len(lastBlock.Columns)-1]
47+
lastColumn.Categories = append(lastColumn.Categories, currentCategory)
48+
currentCategory = nil
49+
}
50+
currentTab = strings.TrimSpace(line[4:])
51+
result = append(result, &BookmarkPage{Tab: currentTab, Blocks: []*BookmarkBlock{{Columns: []*BookmarkColumn{{}}}}})
52+
continue
53+
}
3954
if strings.EqualFold(line, "Page") {
4055
if currentCategory != nil {
4156
currentCategory.Index = idx
@@ -45,7 +60,7 @@ func PreprocessBookmarks(bookmarks string) []*BookmarkPage {
4560
lastColumn.Categories = append(lastColumn.Categories, currentCategory)
4661
currentCategory = nil
4762
}
48-
result = append(result, &BookmarkPage{Blocks: []*BookmarkBlock{{Columns: []*BookmarkColumn{{}}}}})
63+
result = append(result, &BookmarkPage{Tab: currentTab, Blocks: []*BookmarkBlock{{Columns: []*BookmarkColumn{{}}}}})
4964
continue
5065
}
5166
if line == "--" {

bookmarkProcessor_test.go

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,13 @@ type (
1414
Ent = BookmarkEntry
1515
)
1616

17-
func e(u, n string) *Ent { return &Ent{Url: u, Name: n} }
18-
func cat(name string, es ...*Ent) *Cat { return &Cat{Name: name, Entries: es} }
19-
func col(cs ...*Cat) *Col { return &Col{Categories: cs} }
20-
func colsBlock(cs ...*Col) *Blk { return &Blk{Columns: cs} }
21-
func hrBlock() *Blk { return &Blk{HR: true} }
22-
func page(bs ...*Blk) *Pg { return &Pg{Blocks: bs} }
17+
func e(u, n string) *Ent { return &Ent{Url: u, Name: n} }
18+
func cat(name string, es ...*Ent) *Cat { return &Cat{Name: name, Entries: es} }
19+
func col(cs ...*Cat) *Col { return &Col{Categories: cs} }
20+
func colsBlock(cs ...*Col) *Blk { return &Blk{Columns: cs} }
21+
func hrBlock() *Blk { return &Blk{HR: true} }
22+
func page(bs ...*Blk) *Pg { return &Pg{Blocks: bs} }
23+
func tabPage(name string, bs ...*Blk) *Pg { return &Pg{Tab: name, Blocks: bs} }
2324

2425
func Test_preprocessBookmarks(t *testing.T) {
2526
tests := []struct {
@@ -80,13 +81,32 @@ func Test_preprocessBookmarks(t *testing.T) {
8081
),
8182
},
8283
},
84+
{
85+
name: "tabs",
86+
input: "Tab: First\nCategory: A\nTab: Second\nCategory: B\n",
87+
want: []*Pg{
88+
page(colsBlock(col())),
89+
tabPage("First", colsBlock(col(cat("A")))),
90+
tabPage("Second", colsBlock(col(cat("B")))),
91+
},
92+
},
93+
{
94+
name: "tab multiple pages",
95+
input: "Tab: X\nCategory: A\nPage\nCategory: B\n",
96+
want: []*Pg{
97+
page(colsBlock(col())),
98+
tabPage("X", colsBlock(col(cat("A")))),
99+
tabPage("X", colsBlock(col(cat("B")))),
100+
},
101+
},
83102
}
84103

85104
ignore := cmpopts.IgnoreFields(BookmarkCategory{}, "Index")
105+
ignoreTab := cmpopts.IgnoreFields(BookmarkPage{}, "Tab")
86106
for _, tt := range tests {
87107
t.Run(tt.name, func(t *testing.T) {
88108
got := PreprocessBookmarks(tt.input)
89-
if diff := cmp.Diff(tt.want, got, ignore); diff != "" {
109+
if diff := cmp.Diff(tt.want, got, ignore, ignoreTab); diff != "" {
90110
t.Errorf("diff:\n%s", diff)
91111
}
92112
})

bookmarkTabEdit.go

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package gobookmarks
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
)
7+
8+
// ExtractTab returns the text for a tab by name including the 'Tab:' line.
9+
func ExtractTab(bookmarks, name string) (string, error) {
10+
lines := strings.Split(bookmarks, "\n")
11+
start := -1
12+
end := len(lines)
13+
lower := strings.ToLower(name)
14+
for i, line := range lines {
15+
trimmed := strings.TrimSpace(line)
16+
if strings.HasPrefix(strings.ToLower(trimmed), "tab:") {
17+
tabName := strings.TrimSpace(line[4:])
18+
if start == -1 && strings.EqualFold(tabName, lower) {
19+
start = i
20+
} else if start != -1 {
21+
end = i
22+
break
23+
}
24+
}
25+
}
26+
if start == -1 {
27+
return "", fmt.Errorf("tab %s not found", name)
28+
}
29+
return strings.Join(lines[start:end], "\n"), nil
30+
}
31+
32+
// ReplaceTab replaces the tab with name with newName and newText.
33+
// newText should not include the leading 'Tab:' line.
34+
func ReplaceTab(bookmarks, name, newName, newText string) (string, error) {
35+
lines := strings.Split(bookmarks, "\n")
36+
start := -1
37+
end := len(lines)
38+
lower := strings.ToLower(name)
39+
for i, line := range lines {
40+
trimmed := strings.TrimSpace(line)
41+
if strings.HasPrefix(strings.ToLower(trimmed), "tab:") {
42+
tabName := strings.TrimSpace(line[4:])
43+
if start == -1 && strings.EqualFold(tabName, lower) {
44+
start = i
45+
} else if start != -1 {
46+
end = i
47+
break
48+
}
49+
}
50+
}
51+
if start == -1 {
52+
return "", fmt.Errorf("tab %s not found", name)
53+
}
54+
var result []string
55+
result = append(result, lines[:start]...)
56+
result = append(result, "Tab: "+newName)
57+
if newText != "" {
58+
newLines := strings.Split(strings.TrimSuffix(newText, "\n"), "\n")
59+
result = append(result, newLines...)
60+
}
61+
result = append(result, lines[end:]...)
62+
return strings.Join(result, "\n"), nil
63+
}
64+
65+
// AppendTab appends a new tab with name and text to bookmarks.
66+
func AppendTab(bookmarks, name, text string) string {
67+
if !strings.HasSuffix(bookmarks, "\n") {
68+
bookmarks += "\n"
69+
}
70+
bookmarks += "Tab: " + name
71+
if text != "" {
72+
if !strings.HasSuffix(text, "\n") {
73+
text += "\n"
74+
}
75+
bookmarks += "\n" + strings.TrimSuffix(text, "\n")
76+
}
77+
if !strings.HasSuffix(bookmarks, "\n") {
78+
bookmarks += "\n"
79+
} else {
80+
if !strings.HasSuffix(bookmarks, "\n\n") {
81+
bookmarks += ""
82+
}
83+
}
84+
return bookmarks
85+
}

bookmarkTabEdit_test.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package gobookmarks
2+
3+
import "testing"
4+
5+
const tabBookmarkText = `Tab: One
6+
Category: A
7+
--
8+
Tab: Two
9+
Category: B
10+
`
11+
12+
func TestExtractTab(t *testing.T) {
13+
got, err := ExtractTab(tabBookmarkText, "Two")
14+
if err != nil {
15+
t.Fatalf("unexpected err: %v", err)
16+
}
17+
expected := "Tab: Two\nCategory: B\n"
18+
if got != expected {
19+
t.Fatalf("expected %q got %q", expected, got)
20+
}
21+
}
22+
23+
func TestExtractTabError(t *testing.T) {
24+
if _, err := ExtractTab(tabBookmarkText, "X"); err == nil {
25+
t.Fatalf("expected error")
26+
}
27+
}
28+
29+
func TestReplaceTab(t *testing.T) {
30+
updated, err := ReplaceTab(tabBookmarkText, "Two", "Z", "Category: C")
31+
if err != nil {
32+
t.Fatalf("unexpected err: %v", err)
33+
}
34+
expected := "Tab: One\nCategory: A\n--\nTab: Z\nCategory: C"
35+
if updated != expected {
36+
t.Fatalf("expected %q got %q", expected, updated)
37+
}
38+
}
39+
40+
func TestAppendTab(t *testing.T) {
41+
updated := AppendTab("Category: X", "New", "Category: Y")
42+
expected := "Category: X\nTab: New\nCategory: Y\n"
43+
if updated != expected {
44+
t.Fatalf("expected %q got %q", expected, updated)
45+
}
46+
}

cmd/gobookmarks/main.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,11 @@ func main() {
189189
r.HandleFunc("/editCategory", runHandlerChain(CategoryEditSaveAction, redirectToHandlerBranchToRef("/"))).Methods("POST").MatcherFunc(RequiresAnAccount()).MatcherFunc(TaskMatcher("Save"))
190190
r.HandleFunc("/editCategory", runHandlerChain(TaskDoneAutoRefreshPage)).Methods("POST")
191191

192+
r.HandleFunc("/editTab", runTemplate("loginPage.gohtml")).Methods("GET").MatcherFunc(gorillamuxlogic.Not(RequiresAnAccount()))
193+
r.HandleFunc("/editTab", runHandlerChain(EditTabPage)).Methods("GET").MatcherFunc(RequiresAnAccount())
194+
r.HandleFunc("/editTab", runHandlerChain(TabEditSaveAction, redirectToHandlerBranchToRef("/"))).Methods("POST").MatcherFunc(RequiresAnAccount()).MatcherFunc(TaskMatcher("Save"))
195+
r.HandleFunc("/editTab", runHandlerChain(TaskDoneAutoRefreshPage)).Methods("POST")
196+
192197
r.HandleFunc("/history", runTemplate("loginPage.gohtml")).Methods("GET").MatcherFunc(gorillamuxlogic.Not(RequiresAnAccount()))
193198
r.HandleFunc("/history", runTemplate("history.gohtml")).Methods("GET").MatcherFunc(RequiresAnAccount())
194199

data_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ func TestCompileGoHTML(t *testing.T) {
1717
files := []string{
1818
"edit.gohtml",
1919
"editCategory.gohtml",
20+
"editTab.gohtml",
2021
"editNotes.gohtml",
2122
"error.gohtml",
2223
"head.gohtml",
@@ -42,6 +43,9 @@ func testFuncMap() template.FuncMap {
4243
"version": func() string { return "test" },
4344
"OAuth2URL": func() string { return "https://example.com" },
4445
"ref": func() string { return "refs/heads/main" },
46+
"add1": func(i int) int { return i + 1 },
47+
"tab": func() string { return "" },
48+
"bookmarkTabs": func() ([]string, error) { return []string{"tab"}, nil },
4549
"useCssColumns": func() bool { return false },
4650
"loggedIn": func() (bool, error) { return true, nil },
4751
"commitShort": func() string {

funcs.go

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,12 @@ func NewFuncs(r *http.Request) template.FuncMap {
5656
"ref": func() string {
5757
return r.URL.Query().Get("ref")
5858
},
59+
"tab": func() string {
60+
return r.URL.Query().Get("tab")
61+
},
62+
"add1": func(i int) int {
63+
return i + 1
64+
},
5965
"useCssColumns": func() bool {
6066
return UseCssColumns
6167
},
@@ -122,7 +128,57 @@ func NewFuncs(r *http.Request) template.FuncMap {
122128
} else {
123129
bookmark = bookmarks
124130
}
125-
return PreprocessBookmarks(bookmark), nil
131+
pages := PreprocessBookmarks(bookmark)
132+
tabName := r.URL.Query().Get("tab")
133+
if tabName != "" {
134+
var filtered []*BookmarkPage
135+
for _, p := range pages {
136+
if p.Tab == tabName {
137+
filtered = append(filtered, p)
138+
}
139+
}
140+
pages = filtered
141+
} else {
142+
var filtered []*BookmarkPage
143+
for _, p := range pages {
144+
if p.Tab == "" {
145+
filtered = append(filtered, p)
146+
}
147+
}
148+
if len(filtered) > 0 {
149+
pages = filtered
150+
}
151+
}
152+
return pages, nil
153+
},
154+
"bookmarkTabs": func() ([]string, error) {
155+
session := r.Context().Value(ContextValues("session")).(*sessions.Session)
156+
githubUser, _ := session.Values["GithubUser"].(*User)
157+
token, _ := session.Values["Token"].(*oauth2.Token)
158+
159+
login := ""
160+
if githubUser != nil {
161+
login = githubUser.Login
162+
}
163+
164+
bookmarks, _, err := GetBookmarks(r.Context(), login, r.URL.Query().Get("ref"), token)
165+
var bookmark = defaultBookmarks
166+
if err != nil {
167+
// TODO check for error type and if it's not exist, fall through
168+
return nil, fmt.Errorf("bookmarkTabs: %w", err)
169+
} else {
170+
bookmark = bookmarks
171+
}
172+
pages := PreprocessBookmarks(bookmark)
173+
seen := map[string]bool{}
174+
var tabs []string
175+
for _, p := range pages {
176+
if p.Tab != "" && !seen[p.Tab] {
177+
seen[p.Tab] = true
178+
tabs = append(tabs, p.Tab)
179+
}
180+
}
181+
return tabs, nil
126182
},
127183
"bookmarkColumns": func() ([]*BookmarkColumn, error) {
128184
session := r.Context().Value(ContextValues("session")).(*sessions.Session)

0 commit comments

Comments
 (0)