appview/pages/markup: insert anchor link in headings #502

merged
opened by oppi.li targeting master from push-qwnqkqnmovyn
Changed files
+63 -2
appview
pages
+39 -1
appview/pages/markup/markdown.go
···
switch a.rctx.RendererType {
case RendererTypeRepoMarkdown:
switch n := n.(type) {
case *ast.Link:
a.rctx.relativeLinkTransformer(n)
case *ast.Image:
···
}
case RendererTypeDefault:
switch n := n.(type) {
case *ast.Image:
a.rctx.imageFromKnotAstTransformer(n)
a.rctx.camoImageLinkAstTransformer(n)
···
dst := string(link.Destination)
-
if isAbsoluteUrl(dst) {
return
}
···
img.Destination = []byte(rctx.imageFromKnotTransformer(dst))
}
// actualPath decides when to join the file path with the
// current repository directory (essentially only when the link
// destination is relative. if it's absolute then we assume the
···
}
return parsed.IsAbs()
}
···
switch a.rctx.RendererType {
case RendererTypeRepoMarkdown:
switch n := n.(type) {
+
case *ast.Heading:
+
a.rctx.anchorHeadingTransformer(n)
case *ast.Link:
a.rctx.relativeLinkTransformer(n)
case *ast.Image:
···
}
case RendererTypeDefault:
switch n := n.(type) {
+
case *ast.Heading:
+
a.rctx.anchorHeadingTransformer(n)
case *ast.Image:
a.rctx.imageFromKnotAstTransformer(n)
a.rctx.camoImageLinkAstTransformer(n)
···
dst := string(link.Destination)
+
if isAbsoluteUrl(dst) || isFragment(dst) || isMail(dst) {
return
}
···
img.Destination = []byte(rctx.imageFromKnotTransformer(dst))
}
+
func (rctx *RenderContext) anchorHeadingTransformer(h *ast.Heading) {
+
idGeneric, exists := h.AttributeString("id")
+
if !exists {
+
return // no id, nothing to do
+
}
+
id, ok := idGeneric.([]byte)
+
if !ok {
+
return
+
}
+
+
// create anchor link
+
anchor := ast.NewLink()
+
anchor.Destination = fmt.Appendf(nil, "#%s", string(id))
+
anchor.SetAttribute([]byte("class"), []byte("anchor"))
+
+
// create icon text
+
iconText := ast.NewString([]byte("#"))
+
anchor.AppendChild(anchor, iconText)
+
+
// set class on heading
+
h.SetAttribute([]byte("class"), []byte("heading"))
+
+
// append anchor to heading
+
h.AppendChild(h, anchor)
+
}
+
// actualPath decides when to join the file path with the
// current repository directory (essentially only when the link
// destination is relative. if it's absolute then we assume the
···
}
return parsed.IsAbs()
}
+
+
func isFragment(link string) bool {
+
return strings.HasPrefix(link, "#")
+
}
+
+
func isMail(link string) bool {
+
return strings.HasPrefix(link, "mailto:")
+
}
+1
appview/pages/markup/sanitizer.go
···
// for code blocks
policy.AllowAttrs("class").Matching(regexp.MustCompile(`chroma`)).OnElements("pre")
policy.AllowAttrs("class").Matching(regexp.MustCompile(`anchor|footnote-ref|footnote-backref`)).OnElements("a")
policy.AllowAttrs("class").Matching(regexp.MustCompile(strings.Join(slices.Collect(maps.Values(chroma.StandardTypes)), "|"))).OnElements("span")
// centering content
···
// for code blocks
policy.AllowAttrs("class").Matching(regexp.MustCompile(`chroma`)).OnElements("pre")
policy.AllowAttrs("class").Matching(regexp.MustCompile(`anchor|footnote-ref|footnote-backref`)).OnElements("a")
+
policy.AllowAttrs("class").Matching(regexp.MustCompile(`heading`)).OnElements("h1", "h2", "h3", "h4", "h5", "h6", "h7", "h8")
policy.AllowAttrs("class").Matching(regexp.MustCompile(strings.Join(slices.Collect(maps.Values(chroma.StandardTypes)), "|"))).OnElements("span")
// centering content
+23 -1
input.css
···
disabled:before:bg-green-400 dark:disabled:before:bg-green-600;
}
.prose li:has(input) {
-
list-style: none;
.prose a.footnote-backref {
@apply no-underline;
}
···
disabled:before:bg-green-400 dark:disabled:before:bg-green-600;
}
+
.prose hr {
+
@apply my-2;
+
}
+
.prose li:has(input) {
+
@apply list-none;
+
}
+
+
.prose ul:has(input) {
+
@apply pl-2;
+
}
+
+
.prose .heading .anchor {
+
@apply no-underline mx-2 opacity-0;
+
}
+
+
.prose .heading:hover .anchor {
+
@apply opacity-70;
+
}
+
+
.prose .heading .anchor:hover {
+
@apply opacity-70;
+
}
+
.prose a.footnote-backref {
@apply no-underline;
}