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
··· 177 switch a.rctx.RendererType { 178 case RendererTypeRepoMarkdown: 179 switch n := n.(type) { 180 case *ast.Link: 181 a.rctx.relativeLinkTransformer(n) 182 case *ast.Image: ··· 185 } 186 case RendererTypeDefault: 187 switch n := n.(type) { 188 case *ast.Image: 189 a.rctx.imageFromKnotAstTransformer(n) 190 a.rctx.camoImageLinkAstTransformer(n) ··· 199 200 dst := string(link.Destination) 201 202 - if isAbsoluteUrl(dst) { 203 return 204 } 205 ··· 240 img.Destination = []byte(rctx.imageFromKnotTransformer(dst)) 241 } 242 243 // actualPath decides when to join the file path with the 244 // current repository directory (essentially only when the link 245 // destination is relative. if it's absolute then we assume the ··· 259 } 260 return parsed.IsAbs() 261 }
··· 177 switch a.rctx.RendererType { 178 case RendererTypeRepoMarkdown: 179 switch n := n.(type) { 180 + case *ast.Heading: 181 + a.rctx.anchorHeadingTransformer(n) 182 case *ast.Link: 183 a.rctx.relativeLinkTransformer(n) 184 case *ast.Image: ··· 187 } 188 case RendererTypeDefault: 189 switch n := n.(type) { 190 + case *ast.Heading: 191 + a.rctx.anchorHeadingTransformer(n) 192 case *ast.Image: 193 a.rctx.imageFromKnotAstTransformer(n) 194 a.rctx.camoImageLinkAstTransformer(n) ··· 203 204 dst := string(link.Destination) 205 206 + if isAbsoluteUrl(dst) || isFragment(dst) || isMail(dst) { 207 return 208 } 209 ··· 244 img.Destination = []byte(rctx.imageFromKnotTransformer(dst)) 245 } 246 247 + func (rctx *RenderContext) anchorHeadingTransformer(h *ast.Heading) { 248 + idGeneric, exists := h.AttributeString("id") 249 + if !exists { 250 + return // no id, nothing to do 251 + } 252 + id, ok := idGeneric.([]byte) 253 + if !ok { 254 + return 255 + } 256 + 257 + // create anchor link 258 + anchor := ast.NewLink() 259 + anchor.Destination = fmt.Appendf(nil, "#%s", string(id)) 260 + anchor.SetAttribute([]byte("class"), []byte("anchor")) 261 + 262 + // create icon text 263 + iconText := ast.NewString([]byte("#")) 264 + anchor.AppendChild(anchor, iconText) 265 + 266 + // set class on heading 267 + h.SetAttribute([]byte("class"), []byte("heading")) 268 + 269 + // append anchor to heading 270 + h.AppendChild(h, anchor) 271 + } 272 + 273 // actualPath decides when to join the file path with the 274 // current repository directory (essentially only when the link 275 // destination is relative. if it's absolute then we assume the ··· 289 } 290 return parsed.IsAbs() 291 } 292 + 293 + func isFragment(link string) bool { 294 + return strings.HasPrefix(link, "#") 295 + } 296 + 297 + func isMail(link string) bool { 298 + return strings.HasPrefix(link, "mailto:") 299 + }
+1
appview/pages/markup/sanitizer.go
··· 65 // for code blocks 66 policy.AllowAttrs("class").Matching(regexp.MustCompile(`chroma`)).OnElements("pre") 67 policy.AllowAttrs("class").Matching(regexp.MustCompile(`anchor|footnote-ref|footnote-backref`)).OnElements("a") 68 policy.AllowAttrs("class").Matching(regexp.MustCompile(strings.Join(slices.Collect(maps.Values(chroma.StandardTypes)), "|"))).OnElements("span") 69 70 // centering content
··· 65 // for code blocks 66 policy.AllowAttrs("class").Matching(regexp.MustCompile(`chroma`)).OnElements("pre") 67 policy.AllowAttrs("class").Matching(regexp.MustCompile(`anchor|footnote-ref|footnote-backref`)).OnElements("a") 68 + policy.AllowAttrs("class").Matching(regexp.MustCompile(`heading`)).OnElements("h1", "h2", "h3", "h4", "h5", "h6", "h7", "h8") 69 policy.AllowAttrs("class").Matching(regexp.MustCompile(strings.Join(slices.Collect(maps.Values(chroma.StandardTypes)), "|"))).OnElements("span") 70 71 // centering content
+23 -1
input.css
··· 134 disabled:before:bg-green-400 dark:disabled:before:bg-green-600; 135 } 136 137 .prose li:has(input) { 138 - list-style: none; 139 .prose a.footnote-backref { 140 @apply no-underline; 141 }
··· 134 disabled:before:bg-green-400 dark:disabled:before:bg-green-600; 135 } 136 137 + .prose hr { 138 + @apply my-2; 139 + } 140 + 141 .prose li:has(input) { 142 + @apply list-none; 143 + } 144 + 145 + .prose ul:has(input) { 146 + @apply pl-2; 147 + } 148 + 149 + .prose .heading .anchor { 150 + @apply no-underline mx-2 opacity-0; 151 + } 152 + 153 + .prose .heading:hover .anchor { 154 + @apply opacity-70; 155 + } 156 + 157 + .prose .heading .anchor:hover { 158 + @apply opacity-70; 159 + } 160 + 161 .prose a.footnote-backref { 162 @apply no-underline; 163 }