* Improve issue autolinks Update autolinks to match what github does here: Issue in same repo: #1 Issue in different repo: org/repo#1 Fixes #6264 * Use setting.AppURL when parsing URL Using setting.AppURL here is a more reliable way of parsing the current URL and what other functions in this file seem to use. * Make ComposeMetas always return a valid context * Add per repository markdown renderers for better context * Update for use of context metas Now that we include the user and repo name inside context metas, update various code and tests for this new logictags/v1.9.0-rc1
| @@ -469,19 +469,19 @@ func (repo *Repository) mustOwnerName(e Engine) string { | |||
| return repo.OwnerName | |||
| } | |||
| // ComposeMetas composes a map of metas for rendering external issue tracker URL. | |||
| // ComposeMetas composes a map of metas for properly rendering issue links and external issue trackers. | |||
| func (repo *Repository) ComposeMetas() map[string]string { | |||
| unit, err := repo.GetUnit(UnitTypeExternalTracker) | |||
| if err != nil { | |||
| return nil | |||
| } | |||
| if repo.ExternalMetas == nil { | |||
| repo.ExternalMetas = map[string]string{ | |||
| "format": unit.ExternalTrackerConfig().ExternalTrackerFormat, | |||
| "user": repo.MustOwner().Name, | |||
| "repo": repo.Name, | |||
| "user": repo.MustOwner().Name, | |||
| "repo": repo.Name, | |||
| } | |||
| unit, err := repo.GetUnit(UnitTypeExternalTracker) | |||
| if err != nil { | |||
| return repo.ExternalMetas | |||
| } | |||
| repo.ExternalMetas["format"] = unit.ExternalTrackerConfig().ExternalTrackerFormat | |||
| switch unit.ExternalTrackerConfig().ExternalTrackerStyle { | |||
| case markup.IssueNameStyleAlphanumeric: | |||
| repo.ExternalMetas["style"] = markup.IssueNameStyleAlphanumeric | |||
| @@ -20,7 +20,10 @@ func TestRepo(t *testing.T) { | |||
| repo.Owner = &User{Name: "testOwner"} | |||
| repo.Units = nil | |||
| assert.Nil(t, repo.ComposeMetas()) | |||
| metas := repo.ComposeMetas() | |||
| assert.Equal(t, "testRepo", metas["repo"]) | |||
| assert.Equal(t, "testOwner", metas["user"]) | |||
| externalTracker := RepoUnit{ | |||
| Type: UnitTypeExternalTracker, | |||
| @@ -551,20 +551,37 @@ func shortLinkProcessorFull(ctx *postProcessCtx, node *html.Node, noLink bool) { | |||
| } | |||
| func fullIssuePatternProcessor(ctx *postProcessCtx, node *html.Node) { | |||
| if ctx.metas == nil { | |||
| return | |||
| } | |||
| m := getIssueFullPattern().FindStringSubmatchIndex(node.Data) | |||
| if m == nil { | |||
| return | |||
| } | |||
| link := node.Data[m[0]:m[1]] | |||
| id := "#" + node.Data[m[2]:m[3]] | |||
| // TODO if m[4]:m[5] is not nil, then link is to a comment, | |||
| // and we should indicate that in the text somehow | |||
| replaceContent(node, m[0], m[1], createLink(link, id)) | |||
| // extract repo and org name from matched link like | |||
| // http://localhost:3000/gituser/myrepo/issues/1 | |||
| linkParts := strings.Split(path.Clean(link), "/") | |||
| matchOrg := linkParts[len(linkParts)-4] | |||
| matchRepo := linkParts[len(linkParts)-3] | |||
| if matchOrg == ctx.metas["user"] && matchRepo == ctx.metas["repo"] { | |||
| // TODO if m[4]:m[5] is not nil, then link is to a comment, | |||
| // and we should indicate that in the text somehow | |||
| replaceContent(node, m[0], m[1], createLink(link, id)) | |||
| } else { | |||
| orgRepoID := matchOrg + "/" + matchRepo + id | |||
| replaceContent(node, m[0], m[1], createLink(link, orgRepoID)) | |||
| } | |||
| } | |||
| func issueIndexPatternProcessor(ctx *postProcessCtx, node *html.Node) { | |||
| prefix := cutoutVerbosePrefix(ctx.urlPrefix) | |||
| if ctx.metas == nil { | |||
| return | |||
| } | |||
| // default to numeric pattern, unless alphanumeric is requested. | |||
| pattern := issueNumericPattern | |||
| if ctx.metas["style"] == IssueNameStyleAlphanumeric { | |||
| @@ -575,11 +592,10 @@ func issueIndexPatternProcessor(ctx *postProcessCtx, node *html.Node) { | |||
| if match == nil { | |||
| return | |||
| } | |||
| id := node.Data[match[2]:match[3]] | |||
| var link *html.Node | |||
| if ctx.metas == nil { | |||
| link = createLink(util.URLJoin(prefix, "issues", id[1:]), id) | |||
| } else { | |||
| if _, ok := ctx.metas["format"]; ok { | |||
| // Support for external issue tracker | |||
| if ctx.metas["style"] == IssueNameStyleAlphanumeric { | |||
| ctx.metas["index"] = id | |||
| @@ -587,6 +603,8 @@ func issueIndexPatternProcessor(ctx *postProcessCtx, node *html.Node) { | |||
| ctx.metas["index"] = id[1:] | |||
| } | |||
| link = createLink(com.Expand(ctx.metas["format"], ctx.metas), id) | |||
| } else { | |||
| link = createLink(util.URLJoin(setting.AppURL, ctx.metas["user"], ctx.metas["repo"], "issues", id[1:]), id) | |||
| } | |||
| replaceContent(node, match[2], match[3], link) | |||
| } | |||
| @@ -53,6 +53,12 @@ var alphanumericMetas = map[string]string{ | |||
| "style": IssueNameStyleAlphanumeric, | |||
| } | |||
| // these values should match the Repo const above | |||
| var localMetas = map[string]string{ | |||
| "user": "gogits", | |||
| "repo": "gogs", | |||
| } | |||
| func TestRender_IssueIndexPattern(t *testing.T) { | |||
| // numeric: render inputs without valid mentions | |||
| test := func(s string) { | |||
| @@ -91,7 +97,7 @@ func TestRender_IssueIndexPattern2(t *testing.T) { | |||
| links[i] = numericIssueLink(util.URLJoin(setting.AppSubURL, "issues"), index) | |||
| } | |||
| expectedNil := fmt.Sprintf(expectedFmt, links...) | |||
| testRenderIssueIndexPattern(t, s, expectedNil, nil) | |||
| testRenderIssueIndexPattern(t, s, expectedNil, &postProcessCtx{metas: localMetas}) | |||
| for i, index := range indices { | |||
| links[i] = numericIssueLink("https://someurl.com/someUser/someRepo/", index) | |||
| @@ -171,6 +177,7 @@ func testRenderIssueIndexPattern(t *testing.T, input, expected string, ctx *post | |||
| if ctx.urlPrefix == "" { | |||
| ctx.urlPrefix = AppSubURL | |||
| } | |||
| res, err := ctx.postProcess([]byte(input)) | |||
| assert.NoError(t, err) | |||
| assert.Equal(t, expected, string(res)) | |||
| @@ -181,10 +188,10 @@ func TestRender_AutoLink(t *testing.T) { | |||
| setting.AppSubURL = AppSubURL | |||
| test := func(input, expected string) { | |||
| buffer, err := PostProcess([]byte(input), setting.AppSubURL, nil, false) | |||
| buffer, err := PostProcess([]byte(input), setting.AppSubURL, localMetas, false) | |||
| assert.Equal(t, err, nil) | |||
| assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(buffer))) | |||
| buffer, err = PostProcess([]byte(input), setting.AppSubURL, nil, true) | |||
| buffer, err = PostProcess([]byte(input), setting.AppSubURL, localMetas, true) | |||
| assert.Equal(t, err, nil) | |||
| assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(buffer))) | |||
| } | |||
| @@ -214,6 +221,7 @@ func TestRender_FullIssueURLs(t *testing.T) { | |||
| if ctx.urlPrefix == "" { | |||
| ctx.urlPrefix = AppSubURL | |||
| } | |||
| ctx.metas = localMetas | |||
| result, err := ctx.postProcess([]byte(input)) | |||
| assert.NoError(t, err) | |||
| assert.Equal(t, expected, string(result)) | |||
| @@ -221,9 +229,11 @@ func TestRender_FullIssueURLs(t *testing.T) { | |||
| test("Here is a link https://git.osgeo.org/gogs/postgis/postgis/pulls/6", | |||
| "Here is a link https://git.osgeo.org/gogs/postgis/postgis/pulls/6") | |||
| test("Look here http://localhost:3000/person/repo/issues/4", | |||
| `Look here <a href="http://localhost:3000/person/repo/issues/4">#4</a>`) | |||
| `Look here <a href="http://localhost:3000/person/repo/issues/4">person/repo#4</a>`) | |||
| test("http://localhost:3000/person/repo/issues/4#issuecomment-1234", | |||
| `<a href="http://localhost:3000/person/repo/issues/4#issuecomment-1234">#4</a>`) | |||
| `<a href="http://localhost:3000/person/repo/issues/4#issuecomment-1234">person/repo#4</a>`) | |||
| test("http://localhost:3000/gogits/gogs/issues/4", | |||
| `<a href="http://localhost:3000/gogits/gogs/issues/4">#4</a>`) | |||
| } | |||
| func TestRegExp_issueNumericPattern(t *testing.T) { | |||
| @@ -19,6 +19,12 @@ const AppURL = "http://localhost:3000/" | |||
| const Repo = "gogits/gogs" | |||
| const AppSubURL = AppURL + Repo + "/" | |||
| // these values should match the Repo const above | |||
| var localMetas = map[string]string{ | |||
| "user": "gogits", | |||
| "repo": "gogs", | |||
| } | |||
| func TestRender_StandardLinks(t *testing.T) { | |||
| setting.AppURL = AppURL | |||
| setting.AppSubURL = AppSubURL | |||
| @@ -100,7 +106,8 @@ func testAnswers(baseURLContent, baseURLImages string) []string { | |||
| <p>Ideas and codes</p> | |||
| <ul> | |||
| <li>Bezier widget (by <a href="` + AppURL + `r-lyeh" rel="nofollow">@r-lyeh</a>) <a href="http://localhost:3000/ocornut/imgui/issues/786" rel="nofollow">#786</a></li> | |||
| <li>Bezier widget (by <a href="` + AppURL + `r-lyeh" rel="nofollow">@r-lyeh</a>) <a href="http://localhost:3000/ocornut/imgui/issues/786" rel="nofollow">ocornut/imgui#786</a></li> | |||
| <li>Bezier widget (by <a href="` + AppURL + `r-lyeh" rel="nofollow">@r-lyeh</a>) <a href="http://localhost:3000/gogits/gogs/issues/786" rel="nofollow">#786</a></li> | |||
| <li>Node graph editors <a href="https://github.com/ocornut/imgui/issues/306" rel="nofollow">https://github.com/ocornut/imgui/issues/306</a></li> | |||
| <li><a href="` + baseURLContent + `/memory_editor_example" rel="nofollow">Memory Editor</a></li> | |||
| <li><a href="` + baseURLContent + `/plot_var_example" rel="nofollow">Plot var helper</a></li> | |||
| @@ -188,6 +195,7 @@ var sameCases = []string{ | |||
| Ideas and codes | |||
| - Bezier widget (by @r-lyeh) ` + AppURL + `ocornut/imgui/issues/786 | |||
| - Bezier widget (by @r-lyeh) ` + AppURL + `gogits/gogs/issues/786 | |||
| - Node graph editors https://github.com/ocornut/imgui/issues/306 | |||
| - [[Memory Editor|memory_editor_example]] | |||
| - [[Plot var helper|plot_var_example]]`, | |||
| @@ -243,7 +251,7 @@ func TestTotal_RenderWiki(t *testing.T) { | |||
| answers := testAnswers(util.URLJoin(AppSubURL, "wiki/"), util.URLJoin(AppSubURL, "wiki", "raw/")) | |||
| for i := 0; i < len(sameCases); i++ { | |||
| line := RenderWiki([]byte(sameCases[i]), AppSubURL, nil) | |||
| line := RenderWiki([]byte(sameCases[i]), AppSubURL, localMetas) | |||
| assert.Equal(t, answers[i], line) | |||
| } | |||
| @@ -270,7 +278,7 @@ func TestTotal_RenderString(t *testing.T) { | |||
| answers := testAnswers(util.URLJoin(AppSubURL, "src", "master/"), util.URLJoin(AppSubURL, "raw", "master/")) | |||
| for i := 0; i < len(sameCases); i++ { | |||
| line := RenderString(sameCases[i], util.URLJoin(AppSubURL, "src", "master/"), nil) | |||
| line := RenderString(sameCases[i], util.URLJoin(AppSubURL, "src", "master/"), localMetas) | |||
| assert.Equal(t, answers[i], line) | |||
| } | |||
| @@ -584,6 +584,8 @@ func RegisterRoutes(m *macaron.Macaron) { | |||
| Patch(reqToken(), reqRepoWriter(models.UnitTypeIssues, models.UnitTypePullRequests), bind(api.EditLabelOption{}), repo.EditLabel). | |||
| Delete(reqToken(), reqRepoWriter(models.UnitTypeIssues, models.UnitTypePullRequests), repo.DeleteLabel) | |||
| }) | |||
| m.Post("/markdown", bind(api.MarkdownOption{}), misc.Markdown) | |||
| m.Post("/markdown/raw", misc.MarkdownRaw) | |||
| m.Group("/milestones", func() { | |||
| m.Combo("").Get(repo.ListMilestones). | |||
| Post(reqToken(), reqRepoWriter(models.UnitTypeIssues, models.UnitTypePullRequests), bind(api.CreateMilestoneOption{}), repo.CreateMilestone) | |||
| @@ -5,12 +5,16 @@ | |||
| package misc | |||
| import ( | |||
| "strings" | |||
| api "code.gitea.io/sdk/gitea" | |||
| "code.gitea.io/gitea/modules/context" | |||
| "code.gitea.io/gitea/modules/markup/markdown" | |||
| "code.gitea.io/gitea/modules/setting" | |||
| "code.gitea.io/gitea/modules/util" | |||
| "mvdan.cc/xurls/v2" | |||
| ) | |||
| // Markdown render markdown document to HTML | |||
| @@ -45,11 +49,23 @@ func Markdown(ctx *context.APIContext, form api.MarkdownOption) { | |||
| switch form.Mode { | |||
| case "gfm": | |||
| md := []byte(form.Text) | |||
| context := util.URLJoin(setting.AppURL, form.Context) | |||
| urlPrefix := form.Context | |||
| var meta map[string]string | |||
| if !strings.HasPrefix(setting.AppSubURL+"/", urlPrefix) { | |||
| // check if urlPrefix is already set to a URL | |||
| linkRegex, _ := xurls.StrictMatchingScheme("https?://") | |||
| m := linkRegex.FindStringIndex(urlPrefix) | |||
| if m == nil { | |||
| urlPrefix = util.URLJoin(setting.AppURL, form.Context) | |||
| } | |||
| } | |||
| if ctx.Repo != nil && ctx.Repo.Repository != nil { | |||
| meta = ctx.Repo.Repository.ComposeMetas() | |||
| } | |||
| if form.Wiki { | |||
| ctx.Write([]byte(markdown.RenderWiki(md, context, nil))) | |||
| ctx.Write([]byte(markdown.RenderWiki(md, urlPrefix, meta))) | |||
| } else { | |||
| ctx.Write(markdown.Render(md, context, nil)) | |||
| ctx.Write(markdown.Render(md, urlPrefix, meta)) | |||
| } | |||
| default: | |||
| ctx.Write(markdown.RenderRaw([]byte(form.Text), "", false)) | |||
| @@ -202,7 +202,7 @@ | |||
| <div class="ui comment form"> | |||
| <div class="ui top attached tabular menu"> | |||
| <a class="active write item">{{$.i18n.Tr "write"}}</a> | |||
| <a class="preview item" data-url="{{AppSubUrl}}/api/v1/markdown" data-context="{{$.RepoLink}}">{{$.i18n.Tr "preview"}}</a> | |||
| <a class="preview item" data-url="{{$.Repository.APIURL}}/markdown" data-context="{{$.RepoLink}}">{{$.i18n.Tr "preview"}}</a> | |||
| </div> | |||
| <div class="ui bottom attached active write tab segment"> | |||
| <textarea tabindex="1" name="content"></textarea> | |||
| @@ -12,7 +12,7 @@ | |||
| <input type="hidden" name="diff_base_cid"> | |||
| <div class="ui top attached tabular menu" {{if not $.hidden}}onload="assingMenuAttributes(this)" {{end}}data-write="write" data-preview="preview"> | |||
| <a class="active item" data-tab="write">{{$.root.i18n.Tr "write"}}</a> | |||
| <a class="item" data-tab="preview" data-url="{{$.root.AppSubUrl}}/api/v1/markdown" data-context="{{$.root.RepoLink}}">{{$.root.i18n.Tr "preview"}}</a> | |||
| <a class="item" data-tab="preview" data-url="{{$.root.Repository.APIURL}}/markdown" data-context="{{$.root.RepoLink}}">{{$.root.i18n.Tr "preview"}}</a> | |||
| </div> | |||
| <div class="ui bottom attached active tab segment" data-tab="write"> | |||
| <div class="field"> | |||
| @@ -30,13 +30,13 @@ | |||
| <div class="ui top attached tabular menu" data-write="write" data-preview="preview" data-diff="diff"> | |||
| <a class="active item" data-tab="write"><i class="octicon octicon-code"></i> {{if .IsNewFile}}{{.i18n.Tr "repo.editor.new_file"}}{{else}}{{.i18n.Tr "repo.editor.edit_file"}}{{end}}</a> | |||
| {{if not .IsNewFile}} | |||
| <a class="item" data-tab="preview" data-url="{{AppSubUrl}}/api/v1/markdown" data-context="{{.RepoLink}}/src/{{.BranchNameSubURL | EscapePound}}" data-preview-file-modes="{{.PreviewableFileModes}}"><i class="octicon octicon-eye"></i> {{.i18n.Tr "preview"}}</a> | |||
| <a class="item" data-tab="preview" data-url="{{.Repository.APIURL}}/markdown" data-context="{{.RepoLink}}/src/{{.BranchNameSubURL | EscapePound}}" data-preview-file-modes="{{.PreviewableFileModes}}"><i class="octicon octicon-eye"></i> {{.i18n.Tr "preview"}}</a> | |||
| <a class="item" data-tab="diff" data-url="{{.RepoLink}}/_preview/{{.BranchName | EscapePound}}/{{.TreePath | EscapePound}}" data-context="{{.BranchLink}}"><i class="octicon octicon-diff"></i> {{.i18n.Tr "repo.editor.preview_changes"}}</a> | |||
| {{end}} | |||
| </div> | |||
| <div class="ui bottom attached active tab segment" data-tab="write"> | |||
| <textarea id="edit_area" name="content" data-id="repo-{{.Repository.Name}}-{{.TreePath}}" | |||
| data-url="{{AppSubUrl}}/api/v1/markdown" | |||
| data-url="{{.Repository.APIURL}}/markdown" | |||
| data-context="{{.RepoLink}}" | |||
| data-markdown-file-exts="{{.MarkdownFileExts}}" | |||
| data-line-wrap-extensions="{{.LineWrapExtensions}}" | |||
| @@ -1,10 +1,10 @@ | |||
| <div class="field"> | |||
| <div class="ui top attached tabular menu" data-write="write" data-preview="preview"> | |||
| <a class="active item" data-tab="write">{{.i18n.Tr "write"}}</a> | |||
| <a class="item" data-tab="preview" data-url="{{AppSubUrl}}/api/v1/markdown" data-context="{{.RepoLink}}">{{.i18n.Tr "preview"}}</a> | |||
| <a class="item" data-tab="preview" data-url="{{.Repository.APIURL}}/markdown" data-context="{{.RepoLink}}">{{.i18n.Tr "preview"}}</a> | |||
| </div> | |||
| <div class="ui bottom attached active tab segment" data-tab="write"> | |||
| <textarea id="content" class="edit_area js-quick-submit" name="content" tabindex="4" data-id="issue-{{.RepoName}}" data-url="{{AppSubUrl}}/api/v1/markdown" data-context="{{.Repo.RepoLink}}"> | |||
| <textarea id="content" class="edit_area js-quick-submit" name="content" tabindex="4" data-id="issue-{{.RepoName}}" data-url="{{.Repository.APIURL}}/markdown" data-context="{{.Repo.RepoLink}}"> | |||
| {{if .BodyQuery}}{{.BodyQuery}}{{else if .IssueTemplate}}{{.IssueTemplate}}{{else if .PullRequestTemplate}}{{.PullRequestTemplate}}{{else}}{{.content}}{{end}}</textarea> | |||
| </div> | |||
| <div class="ui bottom attached tab segment markdown" data-tab="preview"> | |||
| @@ -156,7 +156,7 @@ | |||
| <div class="ui comment form"> | |||
| <div class="ui top attached tabular menu"> | |||
| <a class="active write item">{{$.i18n.Tr "write"}}</a> | |||
| <a class="preview item" data-url="{{AppSubUrl}}/api/v1/markdown" data-context="{{$.RepoLink}}">{{$.i18n.Tr "preview"}}</a> | |||
| <a class="preview item" data-url="{{$.Repository.APIURL}}/markdown" data-context="{{$.RepoLink}}">{{$.i18n.Tr "preview"}}</a> | |||
| </div> | |||
| <div class="ui bottom attached active write tab segment"> | |||
| <textarea tabindex="1" name="content"></textarea> | |||
| @@ -17,7 +17,7 @@ | |||
| <input name="title" value="{{.title}}" autofocus required> | |||
| </div> | |||
| <div class="field"> | |||
| <textarea class="js-quick-submit" id="edit_area" name="content" data-id="wiki-{{.title}}" data-url="{{AppSubUrl}}/api/v1/markdown" data-context="{{.RepoLink}}/wiki" required>{{if .PageIsWikiEdit}}{{.content}}{{else}}{{.i18n.Tr "repo.wiki.welcome"}}{{end}}</textarea> | |||
| <textarea class="js-quick-submit" id="edit_area" name="content" data-id="wiki-{{.title}}" data-url="{{.Repository.APIURL}}/markdown" data-context="{{.RepoLink}}/wiki" required>{{if .PageIsWikiEdit}}{{.content}}{{else}}{{.i18n.Tr "repo.wiki.welcome"}}{{end}}</textarea> | |||
| </div> | |||
| <div class="field"> | |||
| <input name="message" placeholder="{{.i18n.Tr "repo.wiki.default_commit_message"}}"> | |||