package markup
import (
"bytes"
"fmt"
"html"
"log"
"path"
"path/filepath"
"regexp"
"strings"
"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/text"
"github.com/yuin/goldmark/util"
"gogs.io/gogs/internal/conf"
"gogs.io/gogs/internal/lazyregexp"
"gogs.io/gogs/internal/tool"
)
// IsMarkdownFile reports whether name looks like a Markdown file based on its extension.
func IsMarkdownFile(name string) bool {
extension := strings.ToLower(filepath.Ext(name))
for _, ext := range conf.Markdown.FileExtensions {
if strings.ToLower(ext) == extension {
return true
}
}
return false
}
var (
validLinksPattern = lazyregexp.New(`^[a-z][\w-]+://|^mailto:`)
linkifyURLRegexp = regexp.MustCompile(`^(?:http|https|ftp)://[-a-zA-Z0-9@:%._+~#=]{1,256}(?:\.[a-z]+)?(?::\d+)?(?:[/#?][-a-zA-Z0-9@:%_+.~#$!?&/=();,'\^{}\[\]` + "`" + `]*)?`)
)
func isLink(link []byte) bool {
return validLinksPattern.Match(link)
}
type linkTransformer struct {
urlPrefix string
}
func (t *linkTransformer) Transform(node *ast.Document, reader text.Reader, _ parser.Context) {
_ = ast.Walk(node, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
if !entering {
return ast.WalkContinue, nil
}
if link, ok := n.(*ast.Link); ok {
dest := link.Destination
if len(dest) > 0 && !isLink(dest) && dest[0] != '#' {
link.Destination = []byte(path.Join(t.urlPrefix, string(dest)))
}
}
return ast.WalkContinue, nil
})
}
type gogsRenderer struct {
urlPrefix string
}
func (r *gogsRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
reg.Register(ast.KindAutoLink, r.renderAutoLink)
}
func (r *gogsRenderer) renderAutoLink(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
n := node.(*ast.AutoLink)
if !entering {
return ast.WalkContinue, nil
}
if n.AutoLinkType != ast.AutoLinkURL {
url := n.URL(source)
escaped := html.EscapeString(string(url))
_, _ = fmt.Fprintf(w, `%s`, escaped, escaped)
return ast.WalkContinue, nil
}
link := n.URL(source)
if bytes.HasPrefix(link, []byte(conf.Server.ExternalURL)) {
m := CommitPattern.Find(link)
if m != nil {
m = bytes.TrimSpace(m)
i := bytes.Index(m, []byte("commit/"))
j := bytes.Index(m, []byte("#"))
if j == -1 {
j = len(m)
}
escapedURL := html.EscapeString(string(m))
_, _ = fmt.Fprintf(w, ` %s`, escapedURL, tool.ShortSHA1(string(m[i+7:j])))
return ast.WalkContinue, nil
}
m = IssueFullPattern.Find(link)
if m != nil {
m = bytes.TrimSpace(m)
i := bytes.Index(m, []byte("issues/"))
j := bytes.Index(m, []byte("#"))
if j == -1 {
j = len(m)
}
index := string(m[i+7 : j])
escapedURL := html.EscapeString(string(m))
fullRepoURL := conf.Server.ExternalURL + strings.TrimPrefix(r.urlPrefix, "/")
var href string
if strings.HasPrefix(string(m), fullRepoURL) {
href = fmt.Sprintf(`#%s`, escapedURL, html.EscapeString(index))
} else {
repo := html.EscapeString(string(m[len(conf.Server.ExternalURL) : i-1]))
href = fmt.Sprintf(`%s#%s`, escapedURL, repo, html.EscapeString(index))
}
_, _ = w.WriteString(href)
return ast.WalkContinue, nil
}
}
escapedLink := html.EscapeString(string(link))
_, _ = fmt.Fprintf(w, `%s`, escapedLink, escapedLink)
return ast.WalkContinue, nil
}
// RawMarkdown renders content in Markdown syntax to HTML without handling special links.
func RawMarkdown(body []byte, urlPrefix string) []byte {
extensions := []goldmark.Extender{
extension.Table,
extension.Strikethrough,
extension.TaskList,
extension.NewLinkify(extension.WithLinkifyURLRegexp(linkifyURLRegexp)),
}
if conf.Smartypants.Enabled {
extensions = append(extensions, extension.Typographer)
}
rendererOpts := []renderer.Option{
goldmarkhtml.WithUnsafe(),
renderer.WithNodeRenderers(
util.Prioritized(&gogsRenderer{urlPrefix: urlPrefix}, 0),
),
}
if conf.Markdown.EnableHardLineBreak {
rendererOpts = append(rendererOpts, goldmarkhtml.WithHardWraps())
}
md := goldmark.New(
goldmark.WithExtensions(extensions...),
goldmark.WithParserOptions(
parser.WithASTTransformers(
util.Prioritized(&linkTransformer{urlPrefix: urlPrefix}, 0),
),
),
goldmark.WithRendererOptions(rendererOpts...),
)
var buf bytes.Buffer
if err := md.Convert(body, &buf); err != nil {
log.Printf("markup: failed to convert Markdown: %v", err)
return nil
}
return buf.Bytes()
}
// Markdown takes a string or []byte and renders to HTML in Markdown syntax with special links.
func Markdown(input any, urlPrefix string, metas map[string]string) []byte {
return Render(TypeMarkdown, input, urlPrefix, metas)
}