// Copyright 2017 The Gitea Authors. All rights reserved.
// Copyright 2025 The Forgejo Authors.
// SPDX-License-Identifier: MIT
package markup_test
import (
"context"
"io"
"os"
"strings"
"testing"
"forgejo.org/models/unittest"
"forgejo.org/modules/emoji"
"forgejo.org/modules/git"
"forgejo.org/modules/log"
"forgejo.org/modules/markup"
"forgejo.org/modules/markup/markdown"
"forgejo.org/modules/setting"
"forgejo.org/modules/test"
"forgejo.org/modules/translation"
"forgejo.org/modules/util"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
var localMetas = map[string]string{
"user": "gogits",
"repo": "gogs",
"repoPath": "../../tests/gitea-repositories-meta/user13/repo11.git/",
}
func TestMain(m *testing.M) {
unittest.InitSettings()
if err := git.InitSimple(context.Background()); err != nil {
log.Fatal("git init failed, err: %v", err)
}
os.Exit(m.Run())
}
func TestRender_Commits(t *testing.T) {
defer test.MockVariableValue(&setting.AppURL, markup.TestAppURL)()
test := func(input, expected string) {
buffer, err := markup.RenderString(&markup.RenderContext{
Ctx: git.DefaultContext,
RelativePath: ".md",
Links: markup.Links{
AbsolutePrefix: true,
Base: markup.TestRepoURL,
},
Metas: localMetas,
}, input)
require.NoError(t, err)
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
}
sha := "65f1bf27bc3bf70f64657658635e66094edbcb4d"
shaWithExtra := "65f1bf27bc3bf70f64657658635e66094edbcb4d..."
repo := markup.TestRepoURL
commit := util.URLJoin(repo, "commit", sha)
commitWithExtra := util.URLJoin(repo, "commit", shaWithExtra)
tree := util.URLJoin(repo, "tree", sha, "src")
file := util.URLJoin(repo, "commit", sha, "example.txt")
fileWithExtra := file + ":"
fileWithHash := file + "#L2"
fileWithHasExtra := file + "#L2:"
commitCompare := util.URLJoin(repo, "compare", sha+"..."+sha)
commitCompareWithHash := commitCompare + "#L2"
test(sha, `
65f1bf27bc
`)
test(shaWithExtra, `65f1bf27bc...
`)
test(sha[:7], `65f1bf2
`)
test(sha[:39], `65f1bf27bc
`)
test(commit, `65f1bf27bc
`)
test(commitWithExtra, `65f1bf27bc...
`)
test(tree, `65f1bf27bc/src
`)
test(file, `65f1bf27bc/example.txt
`)
test(fileWithExtra, `65f1bf27bc/example.txt:
`)
test(fileWithHash, `65f1bf27bc/example.txt (L2)
`)
test(fileWithHasExtra, `65f1bf27bc/example.txt (L2):
`)
test(commitCompare, `65f1bf27bc...65f1bf27bc
`)
test(commitCompareWithHash, `65f1bf27bc...65f1bf27bc (L2)
`)
test("commit "+sha, `commit 65f1bf27bc
`)
test("/home/gitea/"+sha, "/home/gitea/"+sha+"
")
test("deadbeef", `deadbeef
`)
test("d27ace93", `d27ace93
`)
test(sha[:14]+".x", ``+sha[:14]+`.x
`)
expected14 := `` + sha[:10] + ``
test(sha[:14]+".", ``+expected14+`.
`)
test(sha[:14]+",", ``+expected14+`,
`)
test("["+sha[:14]+"]", `[`+expected14+`]
`)
fileStrangeChars := util.URLJoin(repo, "src", "commit", "eeb243c3395e1921c5d90e73bd739827251fc99d", "path", "to", "file%20%23.txt")
test(fileStrangeChars, `eeb243c339/path/to/file #.txt
`)
commitLink := util.URLJoin(repo, "src", "commit", "eeb243c3395e1921c5d90e73bd739827251fc99d")
test(commitLink, `eeb243c339
`)
crossCommitLink := util.URLJoin(markup.TestAppURL, "forgejo/forgejo", "src", "commit", "eeb243c3395e1921c5d90e73bd739827251fc99d")
test(crossCommitLink, `forgejo/forgejo@eeb243c339
`)
extCommitLink := util.URLJoin("https://codeberg.org/", markup.TestOrgRepo, "src", "commit", "eeb243c3395e1921c5d90e73bd739827251fc99d")
test(extCommitLink, `codeberg.org/`+markup.TestOrgRepo+`@eeb243c339
`)
}
func TestRender_CrossReferences(t *testing.T) {
defer test.MockVariableValue(&setting.AppURL, markup.TestAppURL)()
test := func(input, expected string) {
buffer, err := markup.RenderString(&markup.RenderContext{
Ctx: git.DefaultContext,
RelativePath: "a.md",
Links: markup.Links{
AbsolutePrefix: true,
Base: setting.AppSubURL,
},
Metas: localMetas,
}, input)
require.NoError(t, err)
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
}
test(
"gogits/gogs#12345",
`gogits/gogs#12345
`)
test(
"go-gitea/gitea#12345",
`go-gitea/gitea#12345
`)
test(
"/home/gitea/go-gitea/gitea#12345",
`/home/gitea/go-gitea/gitea#12345
`)
test(
util.URLJoin(markup.TestAppURL, "gogitea", "gitea", "issues", "12345"),
`gogitea/gitea#12345
`)
test(
util.URLJoin(markup.TestAppURL, "go-gitea", "gitea", "issues", "12345"),
`go-gitea/gitea#12345
`)
test(
util.URLJoin(markup.TestAppURL, "gogitea", "some-repo-name", "issues", "12345"),
`gogitea/some-repo-name#12345
`)
sha := "65f1bf27bc3bf70f64657658635e66094edbcb4d"
urlWithQuery := util.URLJoin(markup.TestAppURL, "forgejo", "some-repo-name", "commit", sha, "README.md") + "?display=source#L1-L5"
test(
urlWithQuery,
``+sha[:10]+`/README.md (L1-L5)
`)
}
func TestRender_links(t *testing.T) {
defer test.MockVariableValue(&setting.AppURL, markup.TestAppURL)()
test := func(input, expected string) {
buffer, err := markup.RenderString(&markup.RenderContext{
Ctx: git.DefaultContext,
RelativePath: "a.md",
Links: markup.Links{
Base: markup.TestRepoURL,
},
}, input)
require.NoError(t, err)
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
}
// Text that should be turned into URL
defaultCustom := setting.Markdown.CustomURLSchemes
setting.Markdown.CustomURLSchemes = []string{"ftp", "magnet"}
markup.InitializeSanitizer()
markup.CustomLinkURLSchemes(setting.Markdown.CustomURLSchemes)
test(
"https://www.example.com",
`https://www.example.com
`)
test(
"http://www.example.com",
`http://www.example.com
`)
test(
"https://example.com",
`https://example.com
`)
test(
"http://example.com",
`http://example.com
`)
test(
"http://foo.com/blah_blah",
`http://foo.com/blah_blah
`)
test(
"http://foo.com/blah_blah/",
`http://foo.com/blah_blah/
`)
test(
"http://www.example.com/wpstyle/?p=364",
`http://www.example.com/wpstyle/?p=364
`)
test(
"https://www.example.com/foo/?bar=baz&inga=42&quux",
`https://www.example.com/foo/?bar=baz&inga=42&quux
`)
test(
"http://142.42.1.1/",
`http://142.42.1.1/
`)
test(
"https://github.com/go-gitea/gitea/?p=aaa/bbb.html#ccc-ddd",
`https://github.com/go-gitea/gitea/?p=aaa/bbb.html#ccc-ddd
`)
test(
"https://en.wikipedia.org/wiki/URL_(disambiguation)",
`https://en.wikipedia.org/wiki/URL_(disambiguation)
`)
test(
"https://foo_bar.example.com/",
`https://foo_bar.example.com/
`)
test(
"https://stackoverflow.com/questions/2896191/what-is-go-used-fore",
`https://stackoverflow.com/questions/2896191/what-is-go-used-fore
`)
test(
"https://username:password@gitea.com",
`https://username:password@gitea.com
`)
test(
"ftp://gitea.com/file.txt",
`ftp://gitea.com/file.txt
`)
test(
"magnet:?xt=urn:btih:5dee65101db281ac9c46344cd6b175cdcadabcde&dn=download",
`magnet:?xt=urn:btih:5dee65101db281ac9c46344cd6b175cdcadabcde&dn=download
`)
// Test that should *not* be turned into URL
test(
"www.example.com",
`www.example.com
`)
test(
"example.com",
`example.com
`)
test(
"test.example.com",
`test.example.com
`)
test(
"http://",
`http://
`)
test(
"https://",
`https://
`)
test(
"://",
`://
`)
test(
"www",
`www
`)
test(
"ftps://gitea.com",
`ftps://gitea.com
`)
// Restore previous settings
setting.Markdown.CustomURLSchemes = defaultCustom
markup.InitializeSanitizer()
markup.CustomLinkURLSchemes(setting.Markdown.CustomURLSchemes)
}
func TestRender_PullReviewCommitLink(t *testing.T) {
defer test.MockVariableValue(&setting.AppURL, markup.TestAppURL)()
sha := "190d9492934af498c3f669d6a2431dc5459e5b20"
prCommitLink := util.URLJoin(markup.TestRepoURL, "pulls", "1", "commits", sha)
assert := func(input, expected, base string) {
buffer, err := markup.RenderString(&markup.RenderContext{
Ctx: git.DefaultContext,
RelativePath: ".md",
Links: markup.Links{
AbsolutePrefix: true,
Base: base,
},
Metas: localMetas,
}, input)
require.NoError(t, err)
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
}
assert(prCommitLink, `!1 (commit `+sha[0:10]+`)
`, markup.TestRepoURL)
prCommitLink = util.URLJoin(markup.TestAppURL, "sub1", "sub2", markup.TestOrgRepo, "pulls", "1", "commits", sha)
assert(
prCommitLink,
`localhost:3000/sub1/sub2/gogits/gogs@!1 (commit `+sha[0:10]+`)
`,
util.URLJoin(markup.TestAppURL, "sub1", "sub2", markup.TestOrgRepo),
)
assert(
prCommitLink,
`localhost:3000/sub1/sub2/gogits/gogs@!1 (commit `+sha[0:10]+`)
`,
markup.TestRepoURL,
)
prCommitLink = "https://codeberg.org/forgejo/forgejo/pulls/7979/commits/4d968c08e0a8d24bd2f3fb2a3a48b37e6d84a327#diff-7649acfa98a9ee3faf0d28b488bbff428317fc72"
assert(prCommitLink, `codeberg.org/forgejo/forgejo@!7979 (commit 4d968c08e0)
`, markup.TestRepoURL)
defer test.MockVariableValue(&setting.AppURL, "https://codeberg.org/")()
assert(prCommitLink, `!7979 (commit 4d968c08e0)
`, "https://codeberg.org/forgejo/forgejo")
}
func TestRender_email(t *testing.T) {
defer test.MockVariableValue(&setting.AppURL, markup.TestAppURL)()
test := func(input, expected string) {
res, err := markup.RenderString(&markup.RenderContext{
Ctx: git.DefaultContext,
RelativePath: "a.md",
Links: markup.Links{
Base: markup.TestRepoURL,
},
}, input)
require.NoError(t, err)
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(res))
}
// Text that should be turned into email link
test(
"info@gitea.com",
`info@gitea.com
`)
test(
"(info@gitea.com)",
`(info@gitea.com)
`)
test(
"[info@gitea.com]",
`[info@gitea.com]
`)
test(
"info@gitea.com.",
`info@gitea.com.
`)
test(
"firstname+lastname@gitea.com",
`firstname+lastname@gitea.com
`)
test(
"send email to info@gitea.co.uk.",
`send email to info@gitea.co.uk.
`)
test(
`j.doe@example.com,
j.doe@example.com.
j.doe@example.com;
j.doe@example.com?
j.doe@example.com!`,
`j.doe@example.com,
j.doe@example.com.
j.doe@example.com;
j.doe@example.com?
j.doe@example.com!
`)
// Test that should *not* be turned into email links
test(
"\"info@gitea.com\"",
`"info@gitea.com"
`)
test(
"/home/gitea/mailstore/info@gitea/com",
`/home/gitea/mailstore/info@gitea/com
`)
test(
"git@try.gitea.io:go-gitea/gitea.git",
`git@try.gitea.io:go-gitea/gitea.git
`)
test(
"gitea@3",
`gitea@3
`)
test(
"gitea@gmail.c",
`gitea@gmail.c
`)
test(
"email@domain@domain.com",
`email@domain@domain.com
`)
test(
"email@domain..com",
`email@domain..com
`)
// Test fediverse handle
test(
"@forgejo@floss.social",
`@forgejo@floss.social
`)
test(
"!forgejo@programming.dev",
`!forgejo@programming.dev
`)
test(
"@#&@forgejo.org",
`@#&@forgejo.org
`)
}
func TestRender_emoji(t *testing.T) {
defer test.MockVariableValue(&setting.AppURL, markup.TestAppURL)()
setting.StaticURLPrefix = markup.TestAppURL
test := func(input, expected string) {
expected = strings.ReplaceAll(expected, "&", "&")
buffer, err := markup.RenderString(&markup.RenderContext{
Ctx: git.DefaultContext,
RelativePath: "a.md",
Links: markup.Links{
Base: markup.TestRepoURL,
},
}, input)
require.NoError(t, err)
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
}
// Make sure we can successfully match every emoji in our dataset with regex
for i := range emoji.GemojiData {
test(
emoji.GemojiData[i].Emoji,
``+emoji.GemojiData[i].Emoji+`
`)
}
for i := range emoji.GemojiData {
test(
":"+emoji.GemojiData[i].Aliases[0]+":",
``+emoji.GemojiData[i].Emoji+`
`)
}
// Text that should be turned into or recognized as emoji
test(
":gitea:",
`
`)
test(
":custom-emoji:",
`:custom-emoji:
`)
setting.UI.CustomEmojisLookup.Add("custom-emoji")
test(
":custom-emoji:",
`
`)
test(
"θΏζ―ε符:1::+1: someπ \U0001f44d:custom-emoji: :gitea:",
`θΏζ―ε符:1:π someπ `+
`π
`+
`
`)
test(
"Some text with π in the middle",
`Some text with π in the middle
`)
test(
"Some text with :smile: in the middle",
`Some text with π in the middle
`)
test(
"Some text with ππ 2 emoji next to each other",
`Some text with ππ 2 emoji next to each other
`)
test(
"ππ€ͺππ€β",
`ππ€ͺππ€β
`)
// should match nothing
test(
"2001:0db8:85a3:0000:0000:8a2e:0370:7334",
`2001:0db8:85a3:0000:0000:8a2e:0370:7334
`)
test(
":not exist:",
`:not exist:
`)
}
func TestRender_ShortLinks(t *testing.T) {
defer test.MockVariableValue(&setting.AppURL, markup.TestAppURL)()
tree := util.URLJoin(markup.TestRepoURL, "src", "master")
test := func(input, expected, expectedWiki string) {
buffer, err := markdown.RenderString(&markup.RenderContext{
Ctx: git.DefaultContext,
Links: markup.Links{
Base: markup.TestRepoURL,
BranchPath: "master",
},
}, input)
require.NoError(t, err)
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(buffer)))
buffer, err = markdown.RenderString(&markup.RenderContext{
Ctx: git.DefaultContext,
Links: markup.Links{
Base: markup.TestRepoURL,
},
Metas: localMetas,
IsWiki: true,
}, input)
require.NoError(t, err)
assert.Equal(t, strings.TrimSpace(expectedWiki), strings.TrimSpace(string(buffer)))
}
mediatree := util.URLJoin(markup.TestRepoURL, "media", "master")
url := util.URLJoin(tree, "Link")
otherURL := util.URLJoin(tree, "Other-Link")
encodedURL := util.URLJoin(tree, "Link%3F")
imgurl := util.URLJoin(mediatree, "Link.jpg")
otherImgurl := util.URLJoin(mediatree, "Link+Other.jpg")
encodedImgurl := util.URLJoin(mediatree, "Link+%23.jpg")
notencodedImgurl := util.URLJoin(mediatree, "some", "path", "Link+#.jpg")
urlWiki := util.URLJoin(markup.TestRepoURL, "wiki", "Link")
otherURLWiki := util.URLJoin(markup.TestRepoURL, "wiki", "Other-Link")
encodedURLWiki := util.URLJoin(markup.TestRepoURL, "wiki", "Link%3F")
imgurlWiki := util.URLJoin(markup.TestRepoURL, "wiki", "raw", "Link.jpg")
otherImgurlWiki := util.URLJoin(markup.TestRepoURL, "wiki", "raw", "Link+Other.jpg")
encodedImgurlWiki := util.URLJoin(markup.TestRepoURL, "wiki", "raw", "Link+%23.jpg")
notencodedImgurlWiki := util.URLJoin(markup.TestRepoURL, "wiki", "raw", "some", "path", "Link+#.jpg")
favicon := "https://forgejo.org/favicon.ico"
test(
"[[Link]]",
`Link
`,
`Link
`)
test(
"[[Link.jpg]]",
`
`,
`
`)
test(
"[["+favicon+"]]",
`
`,
`
`)
test(
"[[Name|Link]]",
`Name
`,
`Name
`)
test(
"[[Name|Link.jpg]]",
`
`,
`
`)
test(
"[[Name|Link.jpg|alt=AltName]]",
`
`,
`
`)
test(
"[[Name|Link.jpg|title=Title]]",
`
`,
`
`)
test(
"[[Name|Link.jpg|alt=AltName|title=Title]]",
`
`,
`
`)
test(
"[[Name|Link.jpg|alt=\"AltName\"|title='Title']]",
`
`,
`
`)
test(
"[[Name|Link Other.jpg|alt=\"AltName\"|title='Title']]",
`
`,
`
`)
test(
"[[Link]] [[Other Link]]",
`Link Other Link
`,
`Link Other Link
`)
test(
"[[Link?]]",
`Link?
`,
`Link?
`)
test(
"[[Link]] [[Other Link]] [[Link?]]",
`Link Other Link Link?
`,
`Link Other Link Link?
`)
test(
"[[Link #.jpg]]",
`
`,
`
`)
test(
"[[Name|Link #.jpg|alt=\"AltName\"|title='Title']]",
`
`,
`
`)
test(
"[[some/path/Link #.jpg]]",
`
`,
`
`)
test(
"[[foobar]]
",
`[[foobar]]
`,
`[[foobar]]
`)
}
func TestRender_RelativeImages(t *testing.T) {
defer test.MockVariableValue(&setting.AppURL, markup.TestAppURL)()
test := func(input, expected, expectedWiki string) {
buffer, err := markdown.RenderString(&markup.RenderContext{
Ctx: git.DefaultContext,
Links: markup.Links{
Base: markup.TestRepoURL,
BranchPath: "master",
},
Metas: localMetas,
}, input)
require.NoError(t, err)
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(buffer)))
buffer, err = markdown.RenderString(&markup.RenderContext{
Ctx: git.DefaultContext,
Links: markup.Links{
Base: markup.TestRepoURL,
},
Metas: localMetas,
IsWiki: true,
}, input)
require.NoError(t, err)
assert.Equal(t, strings.TrimSpace(expectedWiki), strings.TrimSpace(string(buffer)))
}
rawwiki := util.URLJoin(markup.TestRepoURL, "wiki", "raw")
mediatree := util.URLJoin(markup.TestRepoURL, "media", "master")
test(
`
`,
`
`,
`
`)
test(
`
`,
`
`,
`
`)
}
func Test_ParseClusterFuzz(t *testing.T) {
defer test.MockVariableValue(&setting.AppURL, markup.TestAppURL)()
localMetas := map[string]string{
"user": "go-gitea",
"repo": "gitea",
}
data := "