mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2024-12-20 20:07:03 +01:00
Use markdown frontmatter to provide Table of contents, language and frontmatter rendering (#11047)
* Add control for the rendering of the frontmatter * Add control to include a TOC * Add control to set language - allows control of ToC header and CJK glyph choice. Signed-off-by: Andrew Thornton art27@cantab.net
This commit is contained in:
parent
d3fc9c08c8
commit
812cfd0ad9
10 changed files with 509 additions and 16 deletions
1
go.mod
1
go.mod
|
@ -124,6 +124,7 @@ require (
|
|||
gopkg.in/ini.v1 v1.52.0
|
||||
gopkg.in/ldap.v3 v3.0.2
|
||||
gopkg.in/testfixtures.v2 v2.5.0
|
||||
gopkg.in/yaml.v2 v2.2.8
|
||||
mvdan.cc/xurls/v2 v2.1.0
|
||||
strk.kbt.io/projects/go/libravatar v0.0.0-20191008002943-06d1c002b251
|
||||
xorm.io/builder v0.3.7
|
||||
|
|
|
@ -351,6 +351,27 @@ func (ctx *postProcessCtx) visitNode(node *html.Node, visitText bool) {
|
|||
visitText = false
|
||||
} else if node.Data == "code" || node.Data == "pre" {
|
||||
return
|
||||
} else if node.Data == "i" {
|
||||
for _, attr := range node.Attr {
|
||||
if attr.Key != "class" {
|
||||
continue
|
||||
}
|
||||
classes := strings.Split(attr.Val, " ")
|
||||
for i, class := range classes {
|
||||
if class == "icon" {
|
||||
classes[0], classes[i] = classes[i], classes[0]
|
||||
attr.Val = strings.Join(classes, " ")
|
||||
|
||||
// Remove all children of icons
|
||||
child := node.FirstChild
|
||||
for child != nil {
|
||||
node.RemoveChild(child)
|
||||
child = node.FirstChild
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for n := node.FirstChild; n != nil; n = n.NextSibling {
|
||||
ctx.visitNode(n, visitText)
|
||||
|
|
107
modules/markup/markdown/ast.go
Normal file
107
modules/markup/markdown/ast.go
Normal file
|
@ -0,0 +1,107 @@
|
|||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package markdown
|
||||
|
||||
import "github.com/yuin/goldmark/ast"
|
||||
|
||||
// Details is a block that contains Summary and details
|
||||
type Details struct {
|
||||
ast.BaseBlock
|
||||
}
|
||||
|
||||
// Dump implements Node.Dump .
|
||||
func (n *Details) Dump(source []byte, level int) {
|
||||
ast.DumpHelper(n, source, level, nil, nil)
|
||||
}
|
||||
|
||||
// KindDetails is the NodeKind for Details
|
||||
var KindDetails = ast.NewNodeKind("Details")
|
||||
|
||||
// Kind implements Node.Kind.
|
||||
func (n *Details) Kind() ast.NodeKind {
|
||||
return KindDetails
|
||||
}
|
||||
|
||||
// NewDetails returns a new Paragraph node.
|
||||
func NewDetails() *Details {
|
||||
return &Details{
|
||||
BaseBlock: ast.BaseBlock{},
|
||||
}
|
||||
}
|
||||
|
||||
// IsDetails returns true if the given node implements the Details interface,
|
||||
// otherwise false.
|
||||
func IsDetails(node ast.Node) bool {
|
||||
_, ok := node.(*Details)
|
||||
return ok
|
||||
}
|
||||
|
||||
// Summary is a block that contains the summary of details block
|
||||
type Summary struct {
|
||||
ast.BaseBlock
|
||||
}
|
||||
|
||||
// Dump implements Node.Dump .
|
||||
func (n *Summary) Dump(source []byte, level int) {
|
||||
ast.DumpHelper(n, source, level, nil, nil)
|
||||
}
|
||||
|
||||
// KindSummary is the NodeKind for Summary
|
||||
var KindSummary = ast.NewNodeKind("Summary")
|
||||
|
||||
// Kind implements Node.Kind.
|
||||
func (n *Summary) Kind() ast.NodeKind {
|
||||
return KindSummary
|
||||
}
|
||||
|
||||
// NewSummary returns a new Summary node.
|
||||
func NewSummary() *Summary {
|
||||
return &Summary{
|
||||
BaseBlock: ast.BaseBlock{},
|
||||
}
|
||||
}
|
||||
|
||||
// IsSummary returns true if the given node implements the Summary interface,
|
||||
// otherwise false.
|
||||
func IsSummary(node ast.Node) bool {
|
||||
_, ok := node.(*Summary)
|
||||
return ok
|
||||
}
|
||||
|
||||
// Icon is an inline for a fomantic icon
|
||||
type Icon struct {
|
||||
ast.BaseInline
|
||||
Name []byte
|
||||
}
|
||||
|
||||
// Dump implements Node.Dump .
|
||||
func (n *Icon) Dump(source []byte, level int) {
|
||||
m := map[string]string{}
|
||||
m["Name"] = string(n.Name)
|
||||
ast.DumpHelper(n, source, level, m, nil)
|
||||
}
|
||||
|
||||
// KindIcon is the NodeKind for Icon
|
||||
var KindIcon = ast.NewNodeKind("Icon")
|
||||
|
||||
// Kind implements Node.Kind.
|
||||
func (n *Icon) Kind() ast.NodeKind {
|
||||
return KindIcon
|
||||
}
|
||||
|
||||
// NewIcon returns a new Paragraph node.
|
||||
func NewIcon(name string) *Icon {
|
||||
return &Icon{
|
||||
BaseInline: ast.BaseInline{},
|
||||
Name: []byte(name),
|
||||
}
|
||||
}
|
||||
|
||||
// IsIcon returns true if the given node implements the Icon interface,
|
||||
// otherwise false.
|
||||
func IsIcon(node ast.Node) bool {
|
||||
_, ok := node.(*Icon)
|
||||
return ok
|
||||
}
|
|
@ -7,12 +7,16 @@ package markdown
|
|||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/markup"
|
||||
"code.gitea.io/gitea/modules/markup/common"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
giteautil "code.gitea.io/gitea/modules/util"
|
||||
|
||||
meta "github.com/yuin/goldmark-meta"
|
||||
"github.com/yuin/goldmark/ast"
|
||||
east "github.com/yuin/goldmark/extension/ast"
|
||||
"github.com/yuin/goldmark/parser"
|
||||
|
@ -24,17 +28,56 @@ import (
|
|||
|
||||
var byteMailto = []byte("mailto:")
|
||||
|
||||
// GiteaASTTransformer is a default transformer of the goldmark tree.
|
||||
type GiteaASTTransformer struct{}
|
||||
// Header holds the data about a header.
|
||||
type Header struct {
|
||||
Level int
|
||||
Text string
|
||||
ID string
|
||||
}
|
||||
|
||||
// ASTTransformer is a default transformer of the goldmark tree.
|
||||
type ASTTransformer struct{}
|
||||
|
||||
// Transform transforms the given AST tree.
|
||||
func (g *GiteaASTTransformer) Transform(node *ast.Document, reader text.Reader, pc parser.Context) {
|
||||
func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc parser.Context) {
|
||||
metaData := meta.GetItems(pc)
|
||||
firstChild := node.FirstChild()
|
||||
createTOC := false
|
||||
var toc = []Header{}
|
||||
rc := &RenderConfig{
|
||||
Meta: "table",
|
||||
Icon: "table",
|
||||
Lang: "",
|
||||
}
|
||||
if metaData != nil {
|
||||
rc.ToRenderConfig(metaData)
|
||||
|
||||
metaNode := rc.toMetaNode(metaData)
|
||||
if metaNode != nil {
|
||||
node.InsertBefore(node, firstChild, metaNode)
|
||||
}
|
||||
createTOC = rc.TOC
|
||||
toc = make([]Header, 0, 100)
|
||||
}
|
||||
|
||||
_ = ast.Walk(node, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
if !entering {
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
switch v := n.(type) {
|
||||
case *ast.Heading:
|
||||
if createTOC {
|
||||
text := n.Text(reader.Source())
|
||||
header := Header{
|
||||
Text: util.BytesToReadOnlyString(text),
|
||||
Level: v.Level,
|
||||
}
|
||||
if id, found := v.AttributeString("id"); found {
|
||||
header.ID = util.BytesToReadOnlyString(id.([]byte))
|
||||
}
|
||||
toc = append(toc, header)
|
||||
}
|
||||
case *ast.Image:
|
||||
// Images need two things:
|
||||
//
|
||||
|
@ -91,6 +134,21 @@ func (g *GiteaASTTransformer) Transform(node *ast.Document, reader text.Reader,
|
|||
}
|
||||
return ast.WalkContinue, nil
|
||||
})
|
||||
|
||||
if createTOC && len(toc) > 0 {
|
||||
lang := rc.Lang
|
||||
if len(lang) == 0 {
|
||||
lang = setting.Langs[0]
|
||||
}
|
||||
tocNode := createTOCNode(toc, lang)
|
||||
if tocNode != nil {
|
||||
node.InsertBefore(node, firstChild, tocNode)
|
||||
}
|
||||
}
|
||||
|
||||
if len(rc.Lang) > 0 {
|
||||
node.SetAttributeString("lang", []byte(rc.Lang))
|
||||
}
|
||||
}
|
||||
|
||||
type prefixedIDs struct {
|
||||
|
@ -139,10 +197,10 @@ func newPrefixedIDs() *prefixedIDs {
|
|||
}
|
||||
}
|
||||
|
||||
// NewTaskCheckBoxHTMLRenderer creates a TaskCheckBoxHTMLRenderer to render tasklists
|
||||
// NewHTMLRenderer creates a HTMLRenderer to render
|
||||
// in the gitea form.
|
||||
func NewTaskCheckBoxHTMLRenderer(opts ...html.Option) renderer.NodeRenderer {
|
||||
r := &TaskCheckBoxHTMLRenderer{
|
||||
func NewHTMLRenderer(opts ...html.Option) renderer.NodeRenderer {
|
||||
r := &HTMLRenderer{
|
||||
Config: html.NewConfig(),
|
||||
}
|
||||
for _, opt := range opts {
|
||||
|
@ -151,19 +209,109 @@ func NewTaskCheckBoxHTMLRenderer(opts ...html.Option) renderer.NodeRenderer {
|
|||
return r
|
||||
}
|
||||
|
||||
// TaskCheckBoxHTMLRenderer is a renderer.NodeRenderer implementation that
|
||||
// renders checkboxes in list items.
|
||||
// Overrides the default goldmark one to present the gitea format
|
||||
type TaskCheckBoxHTMLRenderer struct {
|
||||
// HTMLRenderer is a renderer.NodeRenderer implementation that
|
||||
// renders gitea specific features.
|
||||
type HTMLRenderer struct {
|
||||
html.Config
|
||||
}
|
||||
|
||||
// RegisterFuncs implements renderer.NodeRenderer.RegisterFuncs.
|
||||
func (r *TaskCheckBoxHTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
|
||||
func (r *HTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
|
||||
reg.Register(ast.KindDocument, r.renderDocument)
|
||||
reg.Register(KindDetails, r.renderDetails)
|
||||
reg.Register(KindSummary, r.renderSummary)
|
||||
reg.Register(KindIcon, r.renderIcon)
|
||||
reg.Register(east.KindTaskCheckBox, r.renderTaskCheckBox)
|
||||
}
|
||||
|
||||
func (r *TaskCheckBoxHTMLRenderer) renderTaskCheckBox(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
func (r *HTMLRenderer) renderDocument(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
log.Info("renderDocument %v", node)
|
||||
n := node.(*ast.Document)
|
||||
|
||||
if val, has := n.AttributeString("lang"); has {
|
||||
var err error
|
||||
if entering {
|
||||
_, err = w.WriteString("<div")
|
||||
if err == nil {
|
||||
_, err = w.WriteString(fmt.Sprintf(` lang=%q`, val))
|
||||
}
|
||||
if err == nil {
|
||||
_, err = w.WriteRune('>')
|
||||
}
|
||||
} else {
|
||||
_, err = w.WriteString("</div>")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return ast.WalkStop, err
|
||||
}
|
||||
}
|
||||
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
func (r *HTMLRenderer) renderDetails(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
var err error
|
||||
if entering {
|
||||
_, err = w.WriteString("<details>")
|
||||
} else {
|
||||
_, err = w.WriteString("</details>")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return ast.WalkStop, err
|
||||
}
|
||||
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
func (r *HTMLRenderer) renderSummary(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
var err error
|
||||
if entering {
|
||||
_, err = w.WriteString("<summary>")
|
||||
} else {
|
||||
_, err = w.WriteString("</summary>")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return ast.WalkStop, err
|
||||
}
|
||||
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
var validNameRE = regexp.MustCompile("^[a-z ]+$")
|
||||
|
||||
func (r *HTMLRenderer) renderIcon(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
if !entering {
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
n := node.(*Icon)
|
||||
|
||||
name := strings.TrimSpace(strings.ToLower(string(n.Name)))
|
||||
|
||||
if len(name) == 0 {
|
||||
// skip this
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
if !validNameRE.MatchString(name) {
|
||||
// skip this
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
var err error
|
||||
_, err = w.WriteString(fmt.Sprintf(`<i class="icon %s"></i>`, name))
|
||||
|
||||
if err != nil {
|
||||
return ast.WalkStop, err
|
||||
}
|
||||
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
func (r *HTMLRenderer) renderTaskCheckBox(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
if !entering {
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
|
|
@ -54,13 +54,13 @@ func RenderRaw(body []byte, urlPrefix string, wikiMarkdown bool) []byte {
|
|||
extension.Ellipsis: nil,
|
||||
}),
|
||||
),
|
||||
meta.New(meta.WithTable()),
|
||||
meta.Meta,
|
||||
),
|
||||
goldmark.WithParserOptions(
|
||||
parser.WithAttribute(),
|
||||
parser.WithAutoHeadingID(),
|
||||
parser.WithASTTransformers(
|
||||
util.Prioritized(&GiteaASTTransformer{}, 10000),
|
||||
util.Prioritized(&ASTTransformer{}, 10000),
|
||||
),
|
||||
),
|
||||
goldmark.WithRendererOptions(
|
||||
|
@ -71,7 +71,7 @@ func RenderRaw(body []byte, urlPrefix string, wikiMarkdown bool) []byte {
|
|||
// Override the original Tasklist renderer!
|
||||
converter.Renderer().AddOptions(
|
||||
renderer.WithNodeRenderers(
|
||||
util.Prioritized(NewTaskCheckBoxHTMLRenderer(), 1000),
|
||||
util.Prioritized(NewHTMLRenderer(), 10),
|
||||
),
|
||||
)
|
||||
|
||||
|
@ -85,7 +85,6 @@ func RenderRaw(body []byte, urlPrefix string, wikiMarkdown bool) []byte {
|
|||
if err := converter.Convert(giteautil.NormalizeEOL(body), &buf, parser.WithContext(pc)); err != nil {
|
||||
log.Error("Unable to render: %v", err)
|
||||
}
|
||||
|
||||
return markup.SanitizeReader(&buf).Bytes()
|
||||
}
|
||||
|
||||
|
|
163
modules/markup/markdown/renderconfig.go
Normal file
163
modules/markup/markdown/renderconfig.go
Normal file
|
@ -0,0 +1,163 @@
|
|||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package markdown
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/yuin/goldmark/ast"
|
||||
east "github.com/yuin/goldmark/extension/ast"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
// RenderConfig represents rendering configuration for this file
|
||||
type RenderConfig struct {
|
||||
Meta string
|
||||
Icon string
|
||||
TOC bool
|
||||
Lang string
|
||||
}
|
||||
|
||||
// ToRenderConfig converts a yaml.MapSlice to a RenderConfig
|
||||
func (rc *RenderConfig) ToRenderConfig(meta yaml.MapSlice) {
|
||||
if meta == nil {
|
||||
return
|
||||
}
|
||||
found := false
|
||||
var giteaMetaControl yaml.MapItem
|
||||
for _, item := range meta {
|
||||
strKey, ok := item.Key.(string)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
strKey = strings.TrimSpace(strings.ToLower(strKey))
|
||||
switch strKey {
|
||||
case "gitea":
|
||||
giteaMetaControl = item
|
||||
found = true
|
||||
case "include_toc":
|
||||
val, ok := item.Value.(bool)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
rc.TOC = val
|
||||
case "lang":
|
||||
val, ok := item.Value.(string)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
val = strings.TrimSpace(val)
|
||||
if len(val) == 0 {
|
||||
continue
|
||||
}
|
||||
rc.Lang = val
|
||||
}
|
||||
}
|
||||
|
||||
if found {
|
||||
switch v := giteaMetaControl.Value.(type) {
|
||||
case string:
|
||||
switch v {
|
||||
case "none":
|
||||
rc.Meta = "none"
|
||||
case "table":
|
||||
rc.Meta = "table"
|
||||
default: // "details"
|
||||
rc.Meta = "details"
|
||||
}
|
||||
case yaml.MapSlice:
|
||||
for _, item := range v {
|
||||
strKey, ok := item.Key.(string)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
strKey = strings.TrimSpace(strings.ToLower(strKey))
|
||||
switch strKey {
|
||||
case "meta":
|
||||
val, ok := item.Value.(string)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
switch strings.TrimSpace(strings.ToLower(val)) {
|
||||
case "none":
|
||||
rc.Meta = "none"
|
||||
case "table":
|
||||
rc.Meta = "table"
|
||||
default: // "details"
|
||||
rc.Meta = "details"
|
||||
}
|
||||
case "details_icon":
|
||||
val, ok := item.Value.(string)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
rc.Icon = strings.TrimSpace(strings.ToLower(val))
|
||||
case "include_toc":
|
||||
val, ok := item.Value.(bool)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
rc.TOC = val
|
||||
case "lang":
|
||||
val, ok := item.Value.(string)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
val = strings.TrimSpace(val)
|
||||
if len(val) == 0 {
|
||||
continue
|
||||
}
|
||||
rc.Lang = val
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (rc *RenderConfig) toMetaNode(meta yaml.MapSlice) ast.Node {
|
||||
switch rc.Meta {
|
||||
case "table":
|
||||
return metaToTable(meta)
|
||||
case "details":
|
||||
return metaToDetails(meta, rc.Icon)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func metaToTable(meta yaml.MapSlice) ast.Node {
|
||||
table := east.NewTable()
|
||||
alignments := []east.Alignment{}
|
||||
for range meta {
|
||||
alignments = append(alignments, east.AlignNone)
|
||||
}
|
||||
row := east.NewTableRow(alignments)
|
||||
for _, item := range meta {
|
||||
cell := east.NewTableCell()
|
||||
cell.AppendChild(cell, ast.NewString([]byte(fmt.Sprintf("%v", item.Key))))
|
||||
row.AppendChild(row, cell)
|
||||
}
|
||||
table.AppendChild(table, east.NewTableHeader(row))
|
||||
|
||||
row = east.NewTableRow(alignments)
|
||||
for _, item := range meta {
|
||||
cell := east.NewTableCell()
|
||||
cell.AppendChild(cell, ast.NewString([]byte(fmt.Sprintf("%v", item.Value))))
|
||||
row.AppendChild(row, cell)
|
||||
}
|
||||
table.AppendChild(table, row)
|
||||
return table
|
||||
}
|
||||
|
||||
func metaToDetails(meta yaml.MapSlice, icon string) ast.Node {
|
||||
details := NewDetails()
|
||||
summary := NewSummary()
|
||||
summary.AppendChild(summary, NewIcon(icon))
|
||||
details.AppendChild(details, summary)
|
||||
details.AppendChild(details, metaToTable(meta))
|
||||
|
||||
return details
|
||||
}
|
49
modules/markup/markdown/toc.go
Normal file
49
modules/markup/markdown/toc.go
Normal file
|
@ -0,0 +1,49 @@
|
|||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package markdown
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"github.com/unknwon/i18n"
|
||||
"github.com/yuin/goldmark/ast"
|
||||
)
|
||||
|
||||
func createTOCNode(toc []Header, lang string) ast.Node {
|
||||
details := NewDetails()
|
||||
summary := NewSummary()
|
||||
|
||||
summary.AppendChild(summary, ast.NewString([]byte(i18n.Tr(lang, "toc"))))
|
||||
details.AppendChild(details, summary)
|
||||
ul := ast.NewList('-')
|
||||
details.AppendChild(details, ul)
|
||||
currentLevel := 6
|
||||
for _, header := range toc {
|
||||
if header.Level < currentLevel {
|
||||
currentLevel = header.Level
|
||||
}
|
||||
}
|
||||
for _, header := range toc {
|
||||
for currentLevel > header.Level {
|
||||
ul = ul.Parent().(*ast.List)
|
||||
currentLevel--
|
||||
}
|
||||
for currentLevel < header.Level {
|
||||
newL := ast.NewList('-')
|
||||
ul.AppendChild(ul, newL)
|
||||
currentLevel++
|
||||
ul = newL
|
||||
}
|
||||
li := ast.NewListItem(currentLevel * 2)
|
||||
a := ast.NewLink()
|
||||
a.Destination = []byte(fmt.Sprintf("#%s", url.PathEscape(header.ID)))
|
||||
a.AppendChild(a, ast.NewString([]byte(header.Text)))
|
||||
li.AppendChild(li, a)
|
||||
ul.AppendChild(ul, li)
|
||||
}
|
||||
|
||||
return details
|
||||
}
|
|
@ -56,6 +56,9 @@ func ReplaceSanitizer() {
|
|||
// Allow classes for task lists
|
||||
sanitizer.policy.AllowAttrs("class").Matching(regexp.MustCompile(`task-list`)).OnElements("ul")
|
||||
|
||||
// Allow icons
|
||||
sanitizer.policy.AllowAttrs("class").Matching(regexp.MustCompile(`^icon(\s+[\p{L}\p{N}_-]+)+$`)).OnElements("i", "span")
|
||||
|
||||
// Allow generally safe attributes
|
||||
generalSafeAttrs := []string{"abbr", "accept", "accept-charset",
|
||||
"accesskey", "action", "align", "alt",
|
||||
|
|
|
@ -19,6 +19,7 @@ create_new = Create…
|
|||
user_profile_and_more = Profile and Settings…
|
||||
signed_in_as = Signed in as
|
||||
enable_javascript = This website works better with JavaScript.
|
||||
toc = Table of Contents
|
||||
|
||||
username = Username
|
||||
email = Email Address
|
||||
|
|
1
vendor/modules.txt
vendored
1
vendor/modules.txt
vendored
|
@ -844,6 +844,7 @@ gopkg.in/toqueteos/substring.v1
|
|||
# gopkg.in/warnings.v0 v0.1.2
|
||||
gopkg.in/warnings.v0
|
||||
# gopkg.in/yaml.v2 v2.2.8
|
||||
## explicit
|
||||
gopkg.in/yaml.v2
|
||||
# mvdan.cc/xurls/v2 v2.1.0
|
||||
## explicit
|
||||
|
|
Loading…
Reference in a new issue