/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package converter
import (
"bytes"
"regexp"
"strings"
"github.com/asaskevich/govalidator"
"github.com/microcosm-cc/bluemonday"
"github.com/segmentfault/pacman/log"
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/extension"
"github.com/yuin/goldmark/parser"
"github.com/yuin/goldmark/renderer"
goldmarkHTML "github.com/yuin/goldmark/renderer/html"
"github.com/yuin/goldmark/util"
)
// Markdown2HTML convert markdown to html
func Markdown2HTML(source string) string {
mdConverter := goldmark.New(
goldmark.WithExtensions(&DangerousHTMLFilterExtension{}, extension.GFM, extension.Footnote),
goldmark.WithParserOptions(
parser.WithAutoHeadingID(),
),
goldmark.WithRendererOptions(
goldmarkHTML.WithHardWraps(),
),
)
var buf bytes.Buffer
if err := mdConverter.Convert([]byte(source), &buf); err != nil {
log.Error(err)
return source
}
html := buf.String()
filter := bluemonday.UGCPolicy()
filter.AllowStyling()
filter.RequireNoFollowOnLinks(false)
filter.RequireParseableURLs(false)
filter.RequireNoFollowOnFullyQualifiedLinks(false)
filter.AllowElements("kbd")
filter.AllowAttrs("title").Matching(regexp.MustCompile(`^[\p{L}\p{N}\s\-_',\[\]!\./\\\(\)]*$|^@embed?$`)).Globally()
filter.AllowAttrs("start").OnElements("ol")
html = strings.TrimSpace(filter.Sanitize(html))
return html
}
// Markdown2BasicHTML convert markdown to html, Only basic syntax can be used
func Markdown2BasicHTML(source string) string {
content := Markdown2HTML(source)
filter := bluemonday.NewPolicy()
filter.AllowElements("p", "b", "br", "strong", "em")
filter.AllowAttrs("src").OnElements("img")
filter.AddSpaceWhenStrippingTag(true)
content = filter.Sanitize(content)
return content
}
type DangerousHTMLFilterExtension struct {
}
func (e *DangerousHTMLFilterExtension) Extend(m goldmark.Markdown) {
m.Renderer().AddOptions(renderer.WithNodeRenderers(
util.Prioritized(&DangerousHTMLRenderer{
Config: goldmarkHTML.NewConfig(),
Filter: bluemonday.UGCPolicy(),
}, 1),
))
}
type DangerousHTMLRenderer struct {
goldmarkHTML.Config
Filter *bluemonday.Policy
}
// RegisterFuncs implements renderer.NodeRenderer.RegisterFuncs.
func (r *DangerousHTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
reg.Register(ast.KindHTMLBlock, r.renderHTMLBlock)
reg.Register(ast.KindRawHTML, r.renderRawHTML)
reg.Register(ast.KindLink, r.renderLink)
reg.Register(ast.KindAutoLink, r.renderAutoLink)
}
func (r *DangerousHTMLRenderer) renderRawHTML(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
if !entering {
return ast.WalkSkipChildren, nil
}
n := node.(*ast.RawHTML)
l := n.Segments.Len()
for i := range l {
segment := n.Segments.At(i)
if string(source[segment.Start:segment.Stop]) == "" || string(source[segment.Start:segment.Stop]) == "" {
_, _ = w.Write(segment.Value(source))
} else {
_, _ = w.Write(r.Filter.SanitizeBytes(segment.Value(source)))
}
}
return ast.WalkSkipChildren, nil
}
func (r *DangerousHTMLRenderer) renderHTMLBlock(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
n := node.(*ast.HTMLBlock)
if entering {
l := n.Lines().Len()
for i := range l {
line := n.Lines().At(i)
r.Writer.SecureWrite(w, line.Value(source))
}
} else if n.HasClosure() {
closure := n.ClosureLine
r.Writer.SecureWrite(w, closure.Value(source))
}
return ast.WalkContinue, nil
}
func (r *DangerousHTMLRenderer) renderLink(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
n := node.(*ast.Link)
if entering && r.renderLinkIsUrl(string(n.Destination)) {
_, _ = w.WriteString("')
} else {
_, _ = w.WriteString("")
}
return ast.WalkContinue, nil
}
func (r *DangerousHTMLRenderer) renderAutoLink(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
n := node.(*ast.AutoLink)
if !entering || !r.renderLinkIsUrl(string(n.URL(source))) {
return ast.WalkContinue, nil
}
_, _ = w.WriteString(`')
} else {
_, _ = w.WriteString(`">`)
}
_, _ = w.Write(util.EscapeHTML(label))
_, _ = w.WriteString(``)
return ast.WalkContinue, nil
}
func (r *DangerousHTMLRenderer) renderLinkIsUrl(verifyURL string) bool {
isURL := govalidator.IsURL(verifyURL)
isPath, _ := regexp.MatchString(`^/`, verifyURL)
return isURL || isPath
}