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) {