diff --git a/assets/assets.qtpl.go b/assets/assets.qtpl.go
index 1b8fd71..160549b 100644
--- a/assets/assets.qtpl.go
+++ b/assets/assets.qtpl.go
@@ -134,6 +134,9 @@ blockquote { margin-left: 0; padding-left: 1rem; }
article { overflow-wrap: break-word; word-wrap: break-word; word-break: break-word; line-height: 150%; }
main h1, main h2, main h3, main h4, main h5, main h6 { margin: 1.5rem 0 0 0; }
+.heading__link { text-decoration: none; display: inline-block; }
+.heading__link::after { width: 1rem; content: "§"; color: transparent; }
+.heading__link:hover::after, .heading__link:active::after { color: #999; }
article p { margin: .5rem 0; }
article ul, ol { padding-left: 1.5rem; margin: .5rem 0; }
article code { padding: .1rem .3rem; border-radius: .25rem; font-size: 90%; }
@@ -291,12 +294,7 @@ mark { background: rgba(130, 80, 30, 5); color: inherit; }
@media screen and (max-width: 800px) {
.hypha-tabs { background-color: #232323; }
}
-@media screen and (min-width: 801px) {
- /* .hypha-tabs__tab { border: 1px #ddd solid; } */
- /* .hypha-tabs__tab_active { border-bottom: 1px white solid; } */
}
-}
-
.backlinks { display: none; }
`)
diff --git a/assets/default.css b/assets/default.css
index d785cfc..9c93b48 100644
--- a/assets/default.css
+++ b/assets/default.css
@@ -109,6 +109,9 @@ blockquote { margin-left: 0; padding-left: 1rem; }
article { overflow-wrap: break-word; word-wrap: break-word; word-break: break-word; line-height: 150%; }
main h1, main h2, main h3, main h4, main h5, main h6 { margin: 1.5rem 0 0 0; }
+.heading__link { text-decoration: none; display: inline-block; }
+.heading__link::after { width: 1rem; content: "§"; color: transparent; }
+.heading__link:hover::after, .heading__link:active::after { color: #999; }
article p { margin: .5rem 0; }
article ul, ol { padding-left: 1.5rem; margin: .5rem 0; }
article code { padding: .1rem .3rem; border-radius: .25rem; font-size: 90%; }
@@ -266,11 +269,6 @@ mark { background: rgba(130, 80, 30, 5); color: inherit; }
@media screen and (max-width: 800px) {
.hypha-tabs { background-color: #232323; }
}
-@media screen and (min-width: 801px) {
- /* .hypha-tabs__tab { border: 1px #ddd solid; } */
- /* .hypha-tabs__tab_active { border-bottom: 1px white solid; } */
}
-}
-
.backlinks { display: none; }
diff --git a/hyphae/hyphae.go b/hyphae/hyphae.go
index ce2a3d9..59e2763 100644
--- a/hyphae/hyphae.go
+++ b/hyphae/hyphae.go
@@ -88,6 +88,10 @@ func (h *Hypha) Delete() {
DecrementCount()
byNamesMutex.Unlock()
h.Unlock()
+
+ for _, outlinkHypha := range h.OutLinks {
+ outlinkHypha.DropBackLink(h)
+ }
}
func (h *Hypha) RenameTo(newName string) {
@@ -113,7 +117,16 @@ func (h *Hypha) MergeIn(oh *Hypha) {
}
}
-// Link related stuff:
+// ## Link related stuff
+// Notes in pseudocode and whatnot:
+// * (Reader h) does not mutate h => safe
+// * (Rename h) reuses the same hypha object => safe
+// * (Unattach h) and (Attach h) do not change (Backlinks h) => safe
+
+// * (Delete h) does not change (Backlinks h), but changes (Outlinks h), removing h from them => make it safe
+// * (Unattach h) and (Attach h) => h may start or stop existing => may change (Outlinks h) => make it safe
+// * (Edit h) => h may start existing => may change (Backlinks h) => make it safe
+// * (Edit h) may add or remove h to or from (Outlinks h) => make it safe
func (h *Hypha) AddOutLink(oh *Hypha) (added bool) {
h.Lock()
@@ -140,3 +153,23 @@ func (h *Hypha) AddBackLink(bh *Hypha) (added bool) {
h.BackLinks = append(h.BackLinks, bh)
return true
}
+
+func (h *Hypha) DropBackLink(bh *Hypha) {
+ h.Lock()
+ defer h.Unlock()
+
+ if len(h.BackLinks) <= 1 {
+ h.BackLinks = make([]*Hypha, 0)
+ return
+ }
+ lastBackLinkIndex := len(h.BackLinks)
+ for i, backlink := range h.BackLinks {
+ if backlink == bh {
+ if i != lastBackLinkIndex {
+ h.BackLinks[i] = h.BackLinks[lastBackLinkIndex]
+ }
+ h.BackLinks = h.BackLinks[:lastBackLinkIndex]
+ return
+ }
+ }
+}
diff --git a/link/link.go b/link/link.go
index 8771ca9..6fe3323 100644
--- a/link/link.go
+++ b/link/link.go
@@ -32,6 +32,8 @@ type Link struct {
Kind LinkType
DestinationUnknown bool
+ // #...
+ Anchor string
Protocol string
// How the link address looked originally in source text.
SrcAddress string
@@ -66,7 +68,7 @@ func (l *Link) Href() string {
case LinkExternal, LinkLocalRoot:
return l.Address
default:
- return "/hypha/" + l.Address
+ return "/hypha/" + l.Address + l.Anchor
}
}
@@ -117,6 +119,10 @@ func From(address, display, hyphaName string) *Link {
case strings.HasPrefix(address, "../"):
link.Kind = LinkLocalHypha
link.Address = util.CanonicalName(path.Join(path.Dir(hyphaName), address[3:]))
+ case strings.HasPrefix(address, "#"):
+ link.Kind = LinkLocalHypha
+ link.Address = util.CanonicalName(hyphaName)
+ link.Anchor = address
default:
link.Kind = LinkLocalHypha
link.Address = util.CanonicalName(address)
diff --git a/markup/lexer.go b/markup/lexer.go
index 607393f..ef178b0 100644
--- a/markup/lexer.go
+++ b/markup/lexer.go
@@ -4,6 +4,8 @@ import (
"fmt"
"html"
"strings"
+
+ "github.com/bouncepaw/mycorrhiza/util"
)
// HyphaExists holds function that checks that a hypha is present.
@@ -83,7 +85,8 @@ func lineToAST(line string, state *GemLexerState, ast *[]Line) {
return strings.HasPrefix(line, token)
}
addHeading := func(i int) {
- addLine(fmt.Sprintf("%s", i, state.id, ParagraphToHtml(state.name, line[i+1:]), i))
+ id := util.LettersNumbersOnly(line[i+1:])
+ addLine(fmt.Sprintf(`%s`, i, state.id, ParagraphToHtml(state.name, line[i+1:]), id, id, i))
}
// Beware! Usage of goto. Some may say it is considered evil but in this case it helped to make a better-structured code.
diff --git a/shroom/rename.go b/shroom/rename.go
index 1f861b2..a2d44da 100644
--- a/shroom/rename.go
+++ b/shroom/rename.go
@@ -89,7 +89,7 @@ func renamingPairs(hyphaeToRename []*hyphae.Hypha, replaceName func(string) stri
renameMap := make(map[string]string)
newNames := make([]string, len(hyphaeToRename))
for _, h := range hyphaeToRename {
- h.RLock()
+ h.Lock()
newNames = append(newNames, replaceName(h.Name))
if h.TextPath != "" {
renameMap[h.TextPath] = replaceName(h.TextPath)
@@ -97,7 +97,7 @@ func renamingPairs(hyphaeToRename []*hyphae.Hypha, replaceName func(string) stri
if h.BinaryPath != "" {
renameMap[h.BinaryPath] = replaceName(h.BinaryPath)
}
- h.RUnlock()
+ h.Unlock()
}
if firstFailure, ok := hyphae.AreFreeNames(newNames...); !ok {
return nil, errors.New("Hypha " + firstFailure + " already exists")
diff --git a/util/util.go b/util/util.go
index d7b90a4..6b7590f 100644
--- a/util/util.go
+++ b/util/util.go
@@ -6,6 +6,7 @@ import (
"net/http"
"regexp"
"strings"
+ "unicode"
)
var (
@@ -22,6 +23,24 @@ var (
GeminiCertPath string
)
+// LettersNumbersOnly keeps letters and numbers only in the given string.
+func LettersNumbersOnly(s string) string {
+ var (
+ ret strings.Builder
+ usedUnderscore bool
+ )
+ for _, r := range s {
+ if unicode.IsLetter(r) || unicode.IsNumber(r) {
+ ret.WriteRune(r)
+ usedUnderscore = false
+ } else if !usedUnderscore {
+ ret.WriteRune('_')
+ usedUnderscore = true
+ }
+ }
+ return strings.Trim(ret.String(), "_")
+}
+
// ShorterPath is used by handlerList to display shorter path to the files. It simply strips WikiDir.
func ShorterPath(path string) string {
if strings.HasPrefix(path, WikiDir) {