diff --git a/files/files.go b/files/files.go
index 1bc4e9d..f957fb6 100644
--- a/files/files.go
+++ b/files/files.go
@@ -71,7 +71,7 @@ func PrepareWikiRoot() error {
paths.userCredentialsJSON = filepath.Join(cfg.WikiDir, "users.json")
paths.tokensJSON = filepath.Join(paths.cacheDir, "tokens.json")
- paths.categoriesJSON = filepath.Join(cfg.WikiDir, "tags.json")
+ paths.categoriesJSON = filepath.Join(cfg.WikiDir, "categories.json")
return nil
}
diff --git a/hyphae/categories/tags.go b/hyphae/categories/categories.go
similarity index 60%
rename from hyphae/categories/tags.go
rename to hyphae/categories/categories.go
index 73315ed..87f21d1 100644
--- a/hyphae/categories/tags.go
+++ b/hyphae/categories/categories.go
@@ -1,4 +1,4 @@
-// Package categories provides category management.
+// Package categories provides category management. All operations in this package are mutexed.
//
// As per the long pondering, this is how categories (cats for short)
// work in Mycorrhiza:
@@ -14,31 +14,43 @@
// exist. If there are 1 or more hyphae in the cat, cat A exists.
package categories
-// For WithHypha and Contents, should the results be sorted?
+import "sync"
-// WithHypha returns what categories have the given hypha.
+// WithHypha returns what categories have the given hypha. The hypha name must be canonical.
func WithHypha(hyphaName string) (categoryList []string) {
- panic("todo")
- return
+ mutex.RLock()
+ defer mutex.RUnlock()
+ return hyphaToCategories[hyphaName].categoryList
}
-// Contents returns what hyphae are in the category. If the returned slice is empty, the category does not exist, and vice versa.
+// Contents returns what hyphae are in the category. If the returned slice is empty, the category does not exist, and vice versa. The category name must be canonical.
func Contents(catName string) (hyphaList []string) {
- panic("todo")
- return
+ mutex.RLock()
+ defer mutex.RUnlock()
+ return categoryToHyphae[catName].hyphaList
}
-// AddHyphaToCategory adds the hypha to the category and updates the records on the disk. If the hypha is already in the category, nothing happens. This operation is async-safe.
+var mutex sync.RWMutex
+
+// AddHyphaToCategory adds the hypha to the category and updates the records on the disk. If the hypha is already in the category, nothing happens.
func AddHyphaToCategory(hyphaName, catName string) {
- for _, cat := range WithHypha(hyphaName) {
- if cat == catName {
- return
- }
+ mutex.Lock()
+ if node, ok := hyphaToCategories[hyphaName]; ok {
+ node.storeCategory(catName)
+ } else {
+ hyphaToCategories[hyphaName] = &hyphaNode{categoryList: []string{catName}}
}
- panic("todo")
+
+ if node, ok := categoryToHyphae[catName]; ok {
+ node.storeHypha(hyphaName)
+ } else {
+ categoryToHyphae[catName] = &categoryNode{hyphaList: []string{hyphaName}}
+ }
+ mutex.Unlock()
}
-// RemoveHyphaFromCategory removes the hypha from the category and updates the records on the disk. If the hypha is not in the category, nothing happens. This operation is async-safe.
+// RemoveHyphaFromCategory removes the hypha from the category and updates the records on the disk. If the hypha is not in the category, nothing happens.
func RemoveHyphaFromCategory(hyphaName, catName string) {
- panic("todo")
+ mutex.Lock()
+ mutex.Unlock()
}
diff --git a/hyphae/categories/files.go b/hyphae/categories/files.go
new file mode 100644
index 0000000..24a7c55
--- /dev/null
+++ b/hyphae/categories/files.go
@@ -0,0 +1,109 @@
+package categories
+
+import (
+ "encoding/json"
+ "github.com/bouncepaw/mycorrhiza/files"
+ "github.com/bouncepaw/mycorrhiza/util"
+ "log"
+ "os"
+)
+
+var categoryToHyphae = map[string]*categoryNode{}
+var hyphaToCategories = map[string]*hyphaNode{}
+
+// InitCategories initializes the category system. Call it after the Structure is initialized. This function might terminate the program in case of a bad mood or filesystem faults.
+func InitCategories() {
+ var (
+ record, err = readCategoriesFromDisk()
+ )
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ for _, cat := range record.Categories {
+ if len(cat.Hyphae) == 0 {
+ continue
+ }
+ cat.Name = util.CanonicalName(cat.Name)
+ for i, hyphaName := range cat.Hyphae {
+ cat.Hyphae[i] = util.CanonicalName(hyphaName)
+ }
+ categoryToHyphae[cat.Name] = &categoryNode{hyphaList: cat.Hyphae}
+ }
+
+ for cat, hyphaeInCat := range categoryToHyphae {
+ for _, hyphaName := range hyphaeInCat.hyphaList {
+ if node, ok := hyphaToCategories[hyphaName]; ok {
+ node.storeCategory(cat)
+ } else {
+ hyphaToCategories[hyphaName] = &hyphaNode{categoryList: []string{cat}}
+ }
+ }
+ }
+
+ log.Println("Found", len(categoryToHyphae), "categories")
+ for cat, catNode := range categoryToHyphae { // TODO: remove when not needed
+ log.Println(cat, "->", catNode.hyphaList)
+ }
+ for hyp, hypNode := range hyphaToCategories {
+ log.Println(hyp, "<-", hypNode.categoryList)
+ }
+}
+
+type categoryNode struct {
+ // TODO: ensure this is sorted
+ hyphaList []string
+}
+
+func (cn *categoryNode) storeHypha(hypname string) {
+ for _, hyphaName := range cn.hyphaList {
+ if hyphaName == hypname {
+ return
+ }
+ }
+ cn.hyphaList = append(cn.hyphaList, hypname)
+}
+
+type hyphaNode struct {
+ // TODO: ensure this is sorted
+ categoryList []string
+}
+
+func (hn *hyphaNode) storeCategory(cat string) {
+ for _, category := range hn.categoryList {
+ if category == cat {
+ return
+ }
+ }
+ hn.categoryList = append(hn.categoryList, cat)
+}
+
+type catFileRecord struct {
+ Categories []catRecord `json:"categories"`
+}
+
+type catRecord struct {
+ Name string `json:"name"`
+ Hyphae []string `json:"hyphae"`
+}
+
+func readCategoriesFromDisk() (catFileRecord, error) {
+ var (
+ record catFileRecord
+ categoriesFile = files.CategoriesJSON()
+ fileContents, err = os.ReadFile(categoriesFile)
+ )
+ if os.IsNotExist(err) {
+ return record, nil
+ }
+ if err != nil {
+ return record, err
+ }
+
+ err = json.Unmarshal(fileContents, &record)
+ if err != nil {
+ return record, err
+ }
+
+ return record, nil
+}
diff --git a/main.go b/main.go
index 1fe5048..07e39a5 100644
--- a/main.go
+++ b/main.go
@@ -5,6 +5,7 @@
package main
import (
+ "github.com/bouncepaw/mycorrhiza/hyphae/categories"
"github.com/bouncepaw/mycorrhiza/migration"
"log"
"os"
@@ -48,6 +49,7 @@ func main() {
history.InitGitRepo()
migration.MigrateRocketsMaybe()
shroom.SetHeaderLinks()
+ categories.InitCategories()
// Static files:
static.InitFS(files.StaticFiles())
diff --git a/static/default.css b/static/default.css
index 6987520..d5f3565 100644
--- a/static/default.css
+++ b/static/default.css
@@ -60,7 +60,7 @@ header { width: 100%; margin-bottom: 1rem; }
.layout { display: grid; grid-template-columns: auto 1fr; column-gap: 1rem; margin: 0 1rem; row-gap: 1rem; }
.main-width { margin: 0; }
main { grid-column: 1 / span 1; grid-row: 1 / span 2; }
- .sibling-hyphae, .markup-toolbar, .help-topics { grid-column: 2 / span 1; grid-row: 1 / span 1; }
+ .sibling-hyphae, .markup-toolbar, .help-topics, .categories-card { grid-column: 2 / span 1; grid-row: 1 / span 1; }
.action-toolbar { grid-column: 2 / span 1; grid-row: 2 / span 1; }
.layout-card { width: 100%; }
.edit-toolbar__buttons {display: grid; }
@@ -74,10 +74,10 @@ header { width: 100%; margin-bottom: 1rem; }
.layout { grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); }
.layout-card {max-width: 18rem;}
.main-width { margin: 0 auto; }
- main { grid-column: 2 / span 1; grid-row: 1 / span 2; }
- .sibling-hyphae, .markup-toolbar, .help-topics { grid-column: 3 / span 1; margin-left: 0; }
+ main { grid-column: 2 / span 1; grid-row: 1 / span 3; }
+ .sibling-hyphae, .markup-toolbar, .help-topics { grid-column: 3 / span 1; margin-left: 0; grid-row: 1 / span 2; }
.markup-toolbar { grid-column: 3 / span 1; grid-row: 1 / span 2; }
- .action-toolbar { grid-column: 1 / span 1; grid-row: 1 / span 1; }
+ .action-toolbar, .categories-card { grid-column: 1 / span 1; grid-row: 1 / span 1; }
.edit-toolbar__buttons { grid-template-columns: 1fr; }
}
@@ -275,6 +275,16 @@ table { border: 0; background-color: #444444; color: #ddd; }
mark { background: rgba(130, 80, 30, 5); color: inherit; }
}
+/*
+ * Categories
+ */
+.categories-card__entries {
+ padding-left: 1rem;
+}
+.categories-card__remove-form {
+ float: right;
+}
+
/*
* Shortcuts
*/
diff --git a/views/categories.go b/views/categories.go
new file mode 100644
index 0000000..4b62fdc
--- /dev/null
+++ b/views/categories.go
@@ -0,0 +1,59 @@
+package views
+
+import (
+ "github.com/bouncepaw/mycorrhiza/hyphae/categories"
+ "github.com/bouncepaw/mycorrhiza/util"
+ "html/template"
+ "log"
+ "strings"
+)
+
+const categoriesCardTmpl = `{{$hyphaName := .HyphaName
+}}`
+
+var categoriesCardT *template.Template
+
+func init() {
+ categoriesCardT = template.Must(template.
+ New("category card").
+ Funcs(template.FuncMap{
+ "beautifulName": util.BeautifulName,
+ }).
+ Parse(categoriesCardTmpl))
+}
+
+func categoryCardHTML(hyphaName string) string {
+ var buf strings.Builder
+ err := categoriesCardT.Execute(&buf, struct {
+ HyphaName string
+ Categories []string
+ }{
+ hyphaName,
+ categories.WithHypha(hyphaName),
+ })
+ if err != nil {
+ log.Println(err)
+ }
+ return buf.String()
+}
diff --git a/views/readers.qtpl b/views/readers.qtpl
index dd0ee53..7e41140 100644
--- a/views/readers.qtpl
+++ b/views/readers.qtpl
@@ -131,6 +131,7 @@ If you rename .prevnext, change the docs too.
{%= hyphaInfo(rq, h) %}
+{%s= categoryCardHTML(h.CanonicalName()) %}
{%= siblingHyphaeHTML(siblings, lc) %}
{%= viewScripts() %}
diff --git a/views/readers.qtpl.go b/views/readers.qtpl.go
index 9e02bd5..9d4c6e4 100644
--- a/views/readers.qtpl.go
+++ b/views/readers.qtpl.go
@@ -469,162 +469,167 @@ func StreamHyphaHTML(qw422016 *qt422016.Writer, rq *http.Request, lc *l18n.Local
`)
//line views/readers.qtpl:134
- streamsiblingHyphaeHTML(qw422016, siblings, lc)
+ qw422016.N().S(categoryCardHTML(h.CanonicalName()))
//line views/readers.qtpl:134
qw422016.N().S(`
+`)
+//line views/readers.qtpl:135
+ streamsiblingHyphaeHTML(qw422016, siblings, lc)
+//line views/readers.qtpl:135
+ qw422016.N().S(`
`)
-//line views/readers.qtpl:136
+//line views/readers.qtpl:137
streamviewScripts(qw422016)
-//line views/readers.qtpl:136
+//line views/readers.qtpl:137
qw422016.N().S(`
`)
-//line views/readers.qtpl:137
+//line views/readers.qtpl:138
}
-//line views/readers.qtpl:137
+//line views/readers.qtpl:138
func WriteHyphaHTML(qq422016 qtio422016.Writer, rq *http.Request, lc *l18n.Localizer, h hyphae.Hypha, contents string) {
-//line views/readers.qtpl:137
+//line views/readers.qtpl:138
qw422016 := qt422016.AcquireWriter(qq422016)
-//line views/readers.qtpl:137
+//line views/readers.qtpl:138
StreamHyphaHTML(qw422016, rq, lc, h, contents)
-//line views/readers.qtpl:137
+//line views/readers.qtpl:138
qt422016.ReleaseWriter(qw422016)
-//line views/readers.qtpl:137
+//line views/readers.qtpl:138
}
-//line views/readers.qtpl:137
+//line views/readers.qtpl:138
func HyphaHTML(rq *http.Request, lc *l18n.Localizer, h hyphae.Hypha, contents string) string {
-//line views/readers.qtpl:137
+//line views/readers.qtpl:138
qb422016 := qt422016.AcquireByteBuffer()
-//line views/readers.qtpl:137
+//line views/readers.qtpl:138
WriteHyphaHTML(qb422016, rq, lc, h, contents)
-//line views/readers.qtpl:137
+//line views/readers.qtpl:138
qs422016 := string(qb422016.B)
-//line views/readers.qtpl:137
+//line views/readers.qtpl:138
qt422016.ReleaseByteBuffer(qb422016)
-//line views/readers.qtpl:137
+//line views/readers.qtpl:138
return qs422016
-//line views/readers.qtpl:137
+//line views/readers.qtpl:138
}
-//line views/readers.qtpl:139
+//line views/readers.qtpl:140
func StreamRevisionHTML(qw422016 *qt422016.Writer, rq *http.Request, lc *l18n.Localizer, h hyphae.Hypha, contents, revHash string) {
-//line views/readers.qtpl:139
+//line views/readers.qtpl:140
qw422016.N().S(`
`)
-//line views/readers.qtpl:149
+//line views/readers.qtpl:150
streamviewScripts(qw422016)
-//line views/readers.qtpl:149
+//line views/readers.qtpl:150
qw422016.N().S(`
`)
-//line views/readers.qtpl:150
+//line views/readers.qtpl:151
}
-//line views/readers.qtpl:150
+//line views/readers.qtpl:151
func WriteRevisionHTML(qq422016 qtio422016.Writer, rq *http.Request, lc *l18n.Localizer, h hyphae.Hypha, contents, revHash string) {
-//line views/readers.qtpl:150
+//line views/readers.qtpl:151
qw422016 := qt422016.AcquireWriter(qq422016)
-//line views/readers.qtpl:150
+//line views/readers.qtpl:151
StreamRevisionHTML(qw422016, rq, lc, h, contents, revHash)
-//line views/readers.qtpl:150
+//line views/readers.qtpl:151
qt422016.ReleaseWriter(qw422016)
-//line views/readers.qtpl:150
+//line views/readers.qtpl:151
}
-//line views/readers.qtpl:150
+//line views/readers.qtpl:151
func RevisionHTML(rq *http.Request, lc *l18n.Localizer, h hyphae.Hypha, contents, revHash string) string {
-//line views/readers.qtpl:150
+//line views/readers.qtpl:151
qb422016 := qt422016.AcquireByteBuffer()
-//line views/readers.qtpl:150
+//line views/readers.qtpl:151
WriteRevisionHTML(qb422016, rq, lc, h, contents, revHash)
-//line views/readers.qtpl:150
+//line views/readers.qtpl:151
qs422016 := string(qb422016.B)
-//line views/readers.qtpl:150
+//line views/readers.qtpl:151
qt422016.ReleaseByteBuffer(qb422016)
-//line views/readers.qtpl:150
+//line views/readers.qtpl:151
return qs422016
-//line views/readers.qtpl:150
+//line views/readers.qtpl:151
}
-//line views/readers.qtpl:152
+//line views/readers.qtpl:153
func streamviewScripts(qw422016 *qt422016.Writer) {
-//line views/readers.qtpl:152
+//line views/readers.qtpl:153
qw422016.N().S(`
`)
-//line views/readers.qtpl:153
+//line views/readers.qtpl:154
for _, scriptPath := range cfg.ViewScripts {
-//line views/readers.qtpl:153
+//line views/readers.qtpl:154
qw422016.N().S(`
`)
-//line views/readers.qtpl:155
+//line views/readers.qtpl:156
}
-//line views/readers.qtpl:155
+//line views/readers.qtpl:156
qw422016.N().S(`
`)
-//line views/readers.qtpl:156
+//line views/readers.qtpl:157
}
-//line views/readers.qtpl:156
+//line views/readers.qtpl:157
func writeviewScripts(qq422016 qtio422016.Writer) {
-//line views/readers.qtpl:156
+//line views/readers.qtpl:157
qw422016 := qt422016.AcquireWriter(qq422016)
-//line views/readers.qtpl:156
+//line views/readers.qtpl:157
streamviewScripts(qw422016)
-//line views/readers.qtpl:156
+//line views/readers.qtpl:157
qt422016.ReleaseWriter(qw422016)
-//line views/readers.qtpl:156
+//line views/readers.qtpl:157
}
-//line views/readers.qtpl:156
+//line views/readers.qtpl:157
func viewScripts() string {
-//line views/readers.qtpl:156
+//line views/readers.qtpl:157
qb422016 := qt422016.AcquireByteBuffer()
-//line views/readers.qtpl:156
+//line views/readers.qtpl:157
writeviewScripts(qb422016)
-//line views/readers.qtpl:156
+//line views/readers.qtpl:157
qs422016 := string(qb422016.B)
-//line views/readers.qtpl:156
+//line views/readers.qtpl:157
qt422016.ReleaseByteBuffer(qb422016)
-//line views/readers.qtpl:156
+//line views/readers.qtpl:157
return qs422016
-//line views/readers.qtpl:156
+//line views/readers.qtpl:157
}