1
0
mirror of https://github.com/osmarks/mycorrhiza.git synced 2025-06-05 20:34:05 +00:00

Categories: Show pre-populated categories

They are useless now, and cannot be edited. Also, not properly styled. You get the idea though.
This commit is contained in:
Timur Ismagilov 2022-03-19 23:57:33 +03:00
parent 94fa2e5688
commit f5cbd5622d
8 changed files with 285 additions and 87 deletions

View File

@ -71,7 +71,7 @@ func PrepareWikiRoot() error {
paths.userCredentialsJSON = filepath.Join(cfg.WikiDir, "users.json") paths.userCredentialsJSON = filepath.Join(cfg.WikiDir, "users.json")
paths.tokensJSON = filepath.Join(paths.cacheDir, "tokens.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 return nil
} }

View File

@ -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) // As per the long pondering, this is how categories (cats for short)
// work in Mycorrhiza: // work in Mycorrhiza:
@ -14,31 +14,43 @@
// exist. If there are 1 or more hyphae in the cat, cat A exists. // exist. If there are 1 or more hyphae in the cat, cat A exists.
package categories 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) { func WithHypha(hyphaName string) (categoryList []string) {
panic("todo") mutex.RLock()
return 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) { func Contents(catName string) (hyphaList []string) {
panic("todo") mutex.RLock()
return 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) { func AddHyphaToCategory(hyphaName, catName string) {
for _, cat := range WithHypha(hyphaName) { mutex.Lock()
if cat == catName { if node, ok := hyphaToCategories[hyphaName]; ok {
return 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) { func RemoveHyphaFromCategory(hyphaName, catName string) {
panic("todo") mutex.Lock()
mutex.Unlock()
} }

109
hyphae/categories/files.go Normal file
View File

@ -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
}

View File

@ -5,6 +5,7 @@
package main package main
import ( import (
"github.com/bouncepaw/mycorrhiza/hyphae/categories"
"github.com/bouncepaw/mycorrhiza/migration" "github.com/bouncepaw/mycorrhiza/migration"
"log" "log"
"os" "os"
@ -48,6 +49,7 @@ func main() {
history.InitGitRepo() history.InitGitRepo()
migration.MigrateRocketsMaybe() migration.MigrateRocketsMaybe()
shroom.SetHeaderLinks() shroom.SetHeaderLinks()
categories.InitCategories()
// Static files: // Static files:
static.InitFS(files.StaticFiles()) static.InitFS(files.StaticFiles())

View File

@ -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; } .layout { display: grid; grid-template-columns: auto 1fr; column-gap: 1rem; margin: 0 1rem; row-gap: 1rem; }
.main-width { margin: 0; } .main-width { margin: 0; }
main { grid-column: 1 / span 1; grid-row: 1 / span 2; } 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; } .action-toolbar { grid-column: 2 / span 1; grid-row: 2 / span 1; }
.layout-card { width: 100%; } .layout-card { width: 100%; }
.edit-toolbar__buttons {display: grid; } .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 { grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); }
.layout-card {max-width: 18rem;} .layout-card {max-width: 18rem;}
.main-width { margin: 0 auto; } .main-width { margin: 0 auto; }
main { grid-column: 2 / span 1; grid-row: 1 / span 2; } 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; } .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; } .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; } .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; } mark { background: rgba(130, 80, 30, 5); color: inherit; }
} }
/*
* Categories
*/
.categories-card__entries {
padding-left: 1rem;
}
.categories-card__remove-form {
float: right;
}
/* /*
* Shortcuts * Shortcuts
*/ */

59
views/categories.go Normal file
View File

@ -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
}}<aside class="layout-card categories-card">
<h2 class="layout-card__title">Categories</h2>
<ul class="categories-card__entries">
{{range .Categories}}
<li class="categories-card__entry">
<a class="categories-card__link" href="/category/{{.}}">{{beautifulName .}}</a>
<form method="POST" action="/remove-from-category" class="categories-card__remove-form">
<input type="hidden" name="cat" value="{{.}}">
<input type="hidden" name="hypha" value="{{$hyphaName}}">
<input type="submit" value="X">
</form>
</li>
{{end}}
<li class="categories-card__entry categories-card__add-to-cat">
<form method="POST" action="/add-to-category" class="categories-card__add-form">
<label for="_cat-name">
<input type="text">
<input type="submit" value="Add to category">
</form>
</li>
</ul>
</aside>`
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()
}

View File

@ -131,6 +131,7 @@ If you rename .prevnext, change the docs too.
{%= hyphaInfo(rq, h) %} {%= hyphaInfo(rq, h) %}
</section> </section>
</main> </main>
{%s= categoryCardHTML(h.CanonicalName()) %}
{%= siblingHyphaeHTML(siblings, lc) %} {%= siblingHyphaeHTML(siblings, lc) %}
</div> </div>
{%= viewScripts() %} {%= viewScripts() %}

View File

@ -469,162 +469,167 @@ func StreamHyphaHTML(qw422016 *qt422016.Writer, rq *http.Request, lc *l18n.Local
</main> </main>
`) `)
//line views/readers.qtpl:134 //line views/readers.qtpl:134
streamsiblingHyphaeHTML(qw422016, siblings, lc) qw422016.N().S(categoryCardHTML(h.CanonicalName()))
//line views/readers.qtpl:134 //line views/readers.qtpl:134
qw422016.N().S(` qw422016.N().S(`
`)
//line views/readers.qtpl:135
streamsiblingHyphaeHTML(qw422016, siblings, lc)
//line views/readers.qtpl:135
qw422016.N().S(`
</div> </div>
`) `)
//line views/readers.qtpl:136 //line views/readers.qtpl:137
streamviewScripts(qw422016) streamviewScripts(qw422016)
//line views/readers.qtpl:136 //line views/readers.qtpl:137
qw422016.N().S(` 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) { 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) qw422016 := qt422016.AcquireWriter(qq422016)
//line views/readers.qtpl:137 //line views/readers.qtpl:138
StreamHyphaHTML(qw422016, rq, lc, h, contents) StreamHyphaHTML(qw422016, rq, lc, h, contents)
//line views/readers.qtpl:137 //line views/readers.qtpl:138
qt422016.ReleaseWriter(qw422016) 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 { 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() qb422016 := qt422016.AcquireByteBuffer()
//line views/readers.qtpl:137 //line views/readers.qtpl:138
WriteHyphaHTML(qb422016, rq, lc, h, contents) WriteHyphaHTML(qb422016, rq, lc, h, contents)
//line views/readers.qtpl:137 //line views/readers.qtpl:138
qs422016 := string(qb422016.B) qs422016 := string(qb422016.B)
//line views/readers.qtpl:137 //line views/readers.qtpl:138
qt422016.ReleaseByteBuffer(qb422016) qt422016.ReleaseByteBuffer(qb422016)
//line views/readers.qtpl:137 //line views/readers.qtpl:138
return qs422016 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) { 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(` qw422016.N().S(`
<div class="layout"> <div class="layout">
<main class="main-width"> <main class="main-width">
<section> <section>
<p>`) <p>`)
//line views/readers.qtpl:143 //line views/readers.qtpl:144
qw422016.E().S(lc.Get("ui.revision_warning")) qw422016.E().S(lc.Get("ui.revision_warning"))
//line views/readers.qtpl:143 //line views/readers.qtpl:144
qw422016.N().S(` <a href="/rev-text/`) qw422016.N().S(` <a href="/rev-text/`)
//line views/readers.qtpl:143 //line views/readers.qtpl:144
qw422016.E().S(revHash) qw422016.E().S(revHash)
//line views/readers.qtpl:143 //line views/readers.qtpl:144
qw422016.N().S(`/`) qw422016.N().S(`/`)
//line views/readers.qtpl:143 //line views/readers.qtpl:144
qw422016.E().S(h.CanonicalName()) qw422016.E().S(h.CanonicalName())
//line views/readers.qtpl:143 //line views/readers.qtpl:144
qw422016.N().S(`">`) qw422016.N().S(`">`)
//line views/readers.qtpl:143 //line views/readers.qtpl:144
qw422016.E().S(lc.Get("ui.revision_link")) qw422016.E().S(lc.Get("ui.revision_link"))
//line views/readers.qtpl:143 //line views/readers.qtpl:144
qw422016.N().S(`</a></p> qw422016.N().S(`</a></p>
`) `)
//line views/readers.qtpl:144 //line views/readers.qtpl:145
qw422016.N().S(NaviTitleHTML(h)) qw422016.N().S(NaviTitleHTML(h))
//line views/readers.qtpl:144 //line views/readers.qtpl:145
qw422016.N().S(` qw422016.N().S(`
`) `)
//line views/readers.qtpl:145 //line views/readers.qtpl:146
qw422016.N().S(contents) qw422016.N().S(contents)
//line views/readers.qtpl:145 //line views/readers.qtpl:146
qw422016.N().S(` qw422016.N().S(`
</section> </section>
</main> </main>
</div> </div>
`) `)
//line views/readers.qtpl:149 //line views/readers.qtpl:150
streamviewScripts(qw422016) streamviewScripts(qw422016)
//line views/readers.qtpl:149 //line views/readers.qtpl:150
qw422016.N().S(` 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) { 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) qw422016 := qt422016.AcquireWriter(qq422016)
//line views/readers.qtpl:150 //line views/readers.qtpl:151
StreamRevisionHTML(qw422016, rq, lc, h, contents, revHash) StreamRevisionHTML(qw422016, rq, lc, h, contents, revHash)
//line views/readers.qtpl:150 //line views/readers.qtpl:151
qt422016.ReleaseWriter(qw422016) 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 { 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() qb422016 := qt422016.AcquireByteBuffer()
//line views/readers.qtpl:150 //line views/readers.qtpl:151
WriteRevisionHTML(qb422016, rq, lc, h, contents, revHash) WriteRevisionHTML(qb422016, rq, lc, h, contents, revHash)
//line views/readers.qtpl:150 //line views/readers.qtpl:151
qs422016 := string(qb422016.B) qs422016 := string(qb422016.B)
//line views/readers.qtpl:150 //line views/readers.qtpl:151
qt422016.ReleaseByteBuffer(qb422016) qt422016.ReleaseByteBuffer(qb422016)
//line views/readers.qtpl:150 //line views/readers.qtpl:151
return qs422016 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) { func streamviewScripts(qw422016 *qt422016.Writer) {
//line views/readers.qtpl:152 //line views/readers.qtpl:153
qw422016.N().S(` qw422016.N().S(`
`) `)
//line views/readers.qtpl:153 //line views/readers.qtpl:154
for _, scriptPath := range cfg.ViewScripts { for _, scriptPath := range cfg.ViewScripts {
//line views/readers.qtpl:153 //line views/readers.qtpl:154
qw422016.N().S(` qw422016.N().S(`
<script src="`) <script src="`)
//line views/readers.qtpl:154 //line views/readers.qtpl:155
qw422016.E().S(scriptPath) qw422016.E().S(scriptPath)
//line views/readers.qtpl:154 //line views/readers.qtpl:155
qw422016.N().S(`"></script> qw422016.N().S(`"></script>
`) `)
//line views/readers.qtpl:155 //line views/readers.qtpl:156
} }
//line views/readers.qtpl:155 //line views/readers.qtpl:156
qw422016.N().S(` 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) { func writeviewScripts(qq422016 qtio422016.Writer) {
//line views/readers.qtpl:156 //line views/readers.qtpl:157
qw422016 := qt422016.AcquireWriter(qq422016) qw422016 := qt422016.AcquireWriter(qq422016)
//line views/readers.qtpl:156 //line views/readers.qtpl:157
streamviewScripts(qw422016) streamviewScripts(qw422016)
//line views/readers.qtpl:156 //line views/readers.qtpl:157
qt422016.ReleaseWriter(qw422016) 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 { func viewScripts() string {
//line views/readers.qtpl:156 //line views/readers.qtpl:157
qb422016 := qt422016.AcquireByteBuffer() qb422016 := qt422016.AcquireByteBuffer()
//line views/readers.qtpl:156 //line views/readers.qtpl:157
writeviewScripts(qb422016) writeviewScripts(qb422016)
//line views/readers.qtpl:156 //line views/readers.qtpl:157
qs422016 := string(qb422016.B) qs422016 := string(qb422016.B)
//line views/readers.qtpl:156 //line views/readers.qtpl:157
qt422016.ReleaseByteBuffer(qb422016) qt422016.ReleaseByteBuffer(qb422016)
//line views/readers.qtpl:156 //line views/readers.qtpl:157
return qs422016 return qs422016
//line views/readers.qtpl:156 //line views/readers.qtpl:157
} }