// Copyright 2017 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT package markup_test import ( "io" "maps" "strings" "testing" "gitea.dev/modules/emoji" "gitea.dev/modules/json" "gitea.dev/modules/markup" "gitea.dev/modules/markup/common" "gitea.dev/modules/markup/markdown" "gitea.dev/modules/public" "gitea.dev/modules/setting" testModule "gitea.dev/modules/test" "gitea.dev/modules/util" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) var ( testRepoOwnerName = "user13" testRepoName = "repo11" localMetas = map[string]string{"user": testRepoOwnerName, "repo": testRepoName} ) func testRenderString(ctx *markup.RenderContext, content string) (string, error) { var buf strings.Builder err := markup.Render(ctx, strings.NewReader(content), &buf) return buf.String(), err } func TestRender_Commits(t *testing.T) { test := func(input, expected string) { rctx := markup.NewTestRenderContext(markup.TestAppURL, localMetas).WithRelativePath("a.md") buffer, err := testRenderString(rctx, input) assert.NoError(t, err) assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) } sha := "65f1bf27bc3bf70f64657658635e66094edbcb4d" repo := markup.TestAppURL + testRepoOwnerName + "/" + testRepoName + "/" commit := repo + "commit/" + sha commitPath := "/user13/repo11/commit/" + sha tree := repo + "tree/" + sha + "/src" file := repo + "commit/" + sha + "/example.txt" fileWithExtra := file + ":" fileWithHash := file + "#L2" fileWithHasExtra := file + "#L2:" commitCompare := repo + "compare/" + sha + "..." + sha commitCompareWithHash := commitCompare + "#L2" test(sha, `
`) test(sha[:7], ``) test(sha[:39], ``) test(commit, ``) test(tree, ``) test(file, ``) test(fileWithExtra, ``) test(fileWithHash, ``) test(fileWithHasExtra, ``) test(commitCompare, ``) test(commitCompareWithHash, ``) test("commit "+sha, `commit 65f1bf27bc
/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+`]
`) } func TestRender_CrossReferences(t *testing.T) { defer testModule.MockVariableValue(&setting.AppURL, markup.TestAppURL)() defer testModule.MockVariableValue(&markup.RenderBehaviorForTesting.DisableAdditionalAttributes, true)() test := func(input, expected string) { rctx := markup.NewTestRenderContext(markup.TestAppURL, localMetas).WithRelativePath("a.md") buffer, err := testRenderString(rctx, input) assert.NoError(t, err) assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) } test( "test-owner/test-repo#12345", ``) test( "go-gitea/gitea#12345", ``) test( "/home/gitea/go-gitea/gitea#12345", `/home/gitea/go-gitea/gitea#12345
`) test( markup.TestAppURL+"gogitea/gitea/issues/12345", ``) test( markup.TestAppURL+"go-gitea/gitea/issues/12345", ``) test( markup.TestAppURL+"gogitea/some-repo-name/issues/12345", ``) inputURL := setting.AppURL + "a/b/commit/0123456789012345678901234567890123456789/foo.txt?a=b#L2-L3" test( inputURL, ``) inputURL = setting.AppURL + "repo/owner/archive/0123456789012345678901234567890123456789.tar.gz" test( inputURL, ``) inputURL = setting.AppURL + "owner/repo/commit/0123456789012345678901234567890123456789.patch?key=val" test( inputURL, ``) } func TestRender_links(t *testing.T) { setting.AppURL = markup.TestAppURL defer testModule.MockVariableValue(&markup.RenderBehaviorForTesting.DisableAdditionalAttributes, true)() test := func(input, expected string) { buffer, err := testRenderString(markup.NewTestRenderContext().WithRelativePath("a.md"), input) assert.NoError(t, err) assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) } oldCustomURLSchemes := setting.Markdown.CustomURLSchemes markup.ResetDefaultSanitizerForTesting() defer func() { setting.Markdown.CustomURLSchemes = oldCustomURLSchemes markup.ResetDefaultSanitizerForTesting() common.InitLinkURLSchemes(oldCustomURLSchemes) }() setting.Markdown.CustomURLSchemes = []string{"ftp", "magnet"} common.InitLinkURLSchemes(setting.Markdown.CustomURLSchemes) for _, testCase := range []struct { input, expected string }{ // Text that should be turned into URL {"https://www.example.com", ``}, {"http://www.example.com", ``}, {"https://example.com", ``}, {"http://example.com", ``}, {"http://foo.com/blah_blah", ``}, {"http://foo.com/blah_blah/", ``}, {"http://www.example.com/wpstyle/?p=364", `http://www.example.com/wpstyle/?p=364
`}, {"https://www.example.com/foo/?bar=baz&inga=42&quux", `https://www.example.com/foo/?bar=baz&inga=42&quux
`}, {"http://142.42.1.1/", ``}, {"https://github.com/go-gitea/gitea/?p=aaa/bbb.html#ccc-ddd", `https://github.com/go-gitea/gitea/?p=aaa/bbb.html#ccc-ddd
`}, {"https://en.wikipedia.org/wiki/URL_(disambiguation)", `https://en.wikipedia.org/wiki/URL_(disambiguation)
`}, {"https://foo_bar.example.com/", ``}, {"https://stackoverflow.com/questions/2896191/what-is-go-used-fore", `https://stackoverflow.com/questions/2896191/what-is-go-used-fore
`}, {"https://username:password@gitea.com", `https://username:password@gitea.com
`}, {"ftp://gitea.com/file.txt", ``}, {"magnet:?xt=urn:btih:5dee65101db281ac9c46344cd6b175cdcadabcde&dn=download", `magnet:?xt=urn:btih:5dee65101db281ac9c46344cd6b175cdcadabcde&dn=download
`}, {`[link](https://example.com)`, ``}, {`[link](mailto:test@example.com)`, ``}, {`[link](javascript:xss)`, `link
`}, // Test that should *not* be turned into URL {"www.example.com", `www.example.com
`}, {"example.com", `example.com
`}, {"test.example.com", `test.example.com
`}, {"http://", `http://
`}, {"https://", `https://
`}, {"://", `://
`}, {"www", `www
`}, {"ftps://gitea.com", `ftps://gitea.com
`}, } { test(testCase.input, testCase.expected) } t.Run("LinkEllipsis", func(t *testing.T) { input := util.EllipsisDisplayString("http://10.1.2.3", 12) assert.Equal(t, "http://10…", input) test(input, "http://10…
") input = util.EllipsisDisplayString("http://10.1.2.3", 13) assert.Equal(t, "http://10.…", input) test(input, "http://10.…
") }) } func TestRender_email(t *testing.T) { setting.AppURL = markup.TestAppURL defer testModule.MockVariableValue(&markup.RenderBehaviorForTesting.DisableAdditionalAttributes, true)() for _, testCase := range []struct { input, expected string }{ // Text that should be turned into email link {"info@gitea.com", ``}, {"(info@gitea.com)", ``}, {"[info@gitea.com]", ``}, {"info@gitea.com.", ``}, {"firstname+lastname@gitea.com", ``}, {"send email to info@gitea.co.uk.", `send email to info@gitea.co.uk.
`}, {`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!
`}, // match GitHub behavior {"email@domain@domain.com", `email@domain@domain.com
`}, // match GitHub behavior {`"info@gitea.com"`, ``}, // Test that should *not* be turned into email links {"/home/gitea/mailstore/info@gitea/com", `/home/gitea/mailstore/info@gitea/com
`}, {"git@try.gitea.io:go-gitea/gitea.git", `git@try.gitea.io:go-gitea/gitea.git
`}, {"https://foo:bar@gitea.io", ``}, {"gitea@3", `gitea@3
`}, {"gitea@gmail.c", `gitea@gmail.c
`}, {"email@domain..com", `email@domain..com
`}, // match GitHub behavior {"?a@d.zz", ``}, {"*a@d.zz", ``}, {"~a@d.zz", ``}, // the following cases don't match GitHub behavior, but they are valid email addresses ... // maybe we should reduce the candidate characters for the "name" part in the future {"a*a@d.zz", ``}, {"a~a@d.zz", ``}, } { res, err := testRenderString(markup.NewTestRenderContext().WithRelativePath("a.md"), testCase.input) assert.NoError(t, err) assert.Equal(t, strings.TrimSpace(testCase.expected), strings.TrimSpace(res), "input: %s", testCase.input) } } func TestRender_emoji(t *testing.T) { defer testModule.MockVariableValue(&setting.AppURL, markup.TestAppURL)() defer testModule.MockVariableValue(&setting.StaticURLPrefix, strings.TrimSuffix(markup.TestAppURL, "/"))() defer testModule.MockVariableValue(&setting.UI.CustomEmojisMap, maps.Clone(setting.UI.CustomEmojisMap))() test := func(input, expected string) { expected = strings.ReplaceAll(expected, "&", "&") buffer, err := testRenderString(markup.NewTestRenderContext().WithRelativePath("a.md"), input) assert.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 data, err := public.AssetFS().ReadFile("assets", "emoji.json") require.NoError(t, err) var emojis []emoji.Emoji require.NoError(t, json.Unmarshal(data, &emojis)) for _, e := range emojis { expected := `` + e.Emoji + `
` test(e.Emoji, expected) test(":"+e.Aliases[0]+":", expected) } // Text that should be turned into or recognized as emoji test( ":gitea:", `
:custom-emoji:
`) setting.UI.CustomEmojisMap["custom-emoji"] = ":custom-emoji:" test( ":custom-emoji:", `
这是字符:1:👍 some🐊 `+
`👍
`+
`
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( "😎🤪🔐🤑❓", `😎🤪🔐🤑❓
`) test( "👍🏽🧑🏽💻👩🏿❤️👩🏿", `👍🏽🧑🏽💻👩🏿❤️👩🏿
`) // should match nothing test(":100:200", `:100:200
`) test("std::thread::something", `std::thread::something
`) test(":not exist:", `:not exist:
`) test("foo `:smile:", "foo `:smile:
") test("foo `:smile:`", `foo :smile:
foo ` 😄
") } func TestRender_ShortLinks(t *testing.T) { setting.AppURL = markup.TestAppURL tree := markup.TestRepoURL + "src/master" url := tree + "/Link" otherURL := tree + "/Other-Link" encodedURL := tree + "/Link%3F" imgurl := tree + "/Link.jpg" otherImgurl := tree + "/Link+Other.jpg" encodedImgurl := tree + "/Link+%23.jpg" notencodedImgurl := tree + "/some/path/Link%20#.jpg" renderableFileURL := tree + "/markdown_file.md" unrenderableFileURL := tree + "/file.zip" favicon := "http://google.com/favicon.ico" for _, testCase := range []struct { input, expected string }{ {"[[Link]]", ``}, {"[[Link.-]]", ``}, {"[[Link.jpg]]", ``}, {"[[" + favicon + "]]", ``}, {"[[Name|Link]]", ``}, {"[[Name|Link.jpg]]", ``}, {"[[Name|Link.jpg|alt=AltName]]", ``}, {"[[Name|Link.jpg|title=Title]]", ``}, {"[[Name|Link.jpg|alt=AltName|title=Title]]", ``}, {"[[Name|Link.jpg|alt=\"AltName\"|title='Title']]", ``}, {"[[Name|Link Other.jpg|alt=\"AltName\"|title='Title']]", ``}, {"[[Link]] [[Other Link]]", ``}, {"[[Link?]]", ``}, {"[[Link]] [[Other Link]] [[Link?]]", ``}, {"[[markdown_file.md]]", ``}, {"[[file.zip]]", ``}, {"[[Link #.jpg]]", ``}, {"[[Name|Link #.jpg|alt=\"AltName\"|title='Title']]", ``}, // FIXME: it's unable to resolve: [[link?k=v]] // FIXME: it is a wrong test case, it is not an image, but a link with anchor "#.jpg" {"[[some/path/Link #.jpg]]", ``}, {"", ``}, } { buffer, err := markdown.RenderString(markup.NewTestRenderContext(tree), testCase.input) assert.NoError(t, err) assert.Equal(t, strings.TrimSpace(testCase.expected), strings.TrimSpace(string(buffer))) } } func Test_ParseClusterFuzz(t *testing.T) { setting.AppURL = markup.TestAppURL localMetas := map[string]string{"user": "go-gitea", "repo": "gitea"} data := "