diff options
author | Brecht Van Lommel <brecht@blender.org> | 2023-03-24 07:12:23 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-03-24 14:12:23 +0800 |
commit | 84daddc2fa74393cdc13371b0cc44f0444cfdae0 (patch) | |
tree | eaf75d3173ee67d65288accef86bbb7c6c08535e /routers | |
parent | 9e04627acaaa853e5269f98f53f2615077cfb028 (diff) | |
download | gitea-84daddc2fa74393cdc13371b0cc44f0444cfdae0.tar.gz gitea-84daddc2fa74393cdc13371b0cc44f0444cfdae0.zip |
Editor preview support for external renderers (#23333)
Remove `[repository.editor] PREVIEWABLE_FILE_MODES` setting that seemed
like it was intended to support this but did not work. Instead, whenever
viewing a file shows a preview, also have a Preview tab in the file
editor.
Add new `/markup` web and API endpoints with `comment`, `gfm`,
`markdown` and new `file` mode that uses a file path to determine the
renderer.
Remove `/markdown` web endpoint but keep the API for backwards and
GitHub compatibility.
## ⚠️ BREAKING ⚠️
The `[repository.editor] PREVIEWABLE_FILE_MODES` setting was removed.
This setting served no practical purpose and was not working correctly.
Instead a preview tab is always shown in the file editor when supported.
---------
Co-authored-by: zeripath <art27@cantab.net>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Diffstat (limited to 'routers')
-rw-r--r-- | routers/api/v1/api.go | 2 | ||||
-rw-r--r-- | routers/api/v1/misc/markup.go (renamed from routers/api/v1/misc/markdown.go) | 87 | ||||
-rw-r--r-- | routers/api/v1/misc/markup_test.go (renamed from routers/api/v1/misc/markdown_test.go) | 108 | ||||
-rw-r--r-- | routers/api/v1/swagger/options.go | 2 | ||||
-rw-r--r-- | routers/common/markup.go | 92 | ||||
-rw-r--r-- | routers/web/misc/markdown.go | 98 | ||||
-rw-r--r-- | routers/web/misc/markup.go | 44 | ||||
-rw-r--r-- | routers/web/repo/editor.go | 7 | ||||
-rw-r--r-- | routers/web/web.go | 2 |
9 files changed, 259 insertions, 183 deletions
diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index c3a875e737..8fd824640f 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -711,6 +711,7 @@ func Routes(ctx gocontext.Context) *web.Route { }) } m.Get("/signing-key.gpg", misc.SigningKey) + m.Post("/markup", bind(api.MarkupOption{}), misc.Markup) m.Post("/markdown", bind(api.MarkdownOption{}), misc.Markdown) m.Post("/markdown/raw", misc.MarkdownRaw) m.Group("/settings", func() { @@ -1034,6 +1035,7 @@ func Routes(ctx gocontext.Context) *web.Route { Patch(reqToken(auth_model.AccessTokenScopeRepo), reqRepoWriter(unit.TypeIssues, unit.TypePullRequests), bind(api.EditLabelOption{}), repo.EditLabel). Delete(reqToken(auth_model.AccessTokenScopeRepo), reqRepoWriter(unit.TypeIssues, unit.TypePullRequests), repo.DeleteLabel) }) + m.Post("/markup", reqToken(auth_model.AccessTokenScopeRepo), bind(api.MarkupOption{}), misc.Markup) m.Post("/markdown", reqToken(auth_model.AccessTokenScopeRepo), bind(api.MarkdownOption{}), misc.Markdown) m.Post("/markdown/raw", reqToken(auth_model.AccessTokenScopeRepo), misc.MarkdownRaw) m.Group("/milestones", func() { diff --git a/routers/api/v1/misc/markdown.go b/routers/api/v1/misc/markup.go index 3ff42f08d6..93d5754444 100644 --- a/routers/api/v1/misc/markdown.go +++ b/routers/api/v1/misc/markup.go @@ -5,19 +5,45 @@ package misc import ( "net/http" - "strings" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/markup/markdown" - "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" - "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" - - "mvdan.cc/xurls/v2" + "code.gitea.io/gitea/routers/common" ) +// Markup render markup document to HTML +func Markup(ctx *context.APIContext) { + // swagger:operation POST /markup miscellaneous renderMarkup + // --- + // summary: Render a markup document as HTML + // parameters: + // - name: body + // in: body + // schema: + // "$ref": "#/definitions/MarkupOption" + // consumes: + // - application/json + // produces: + // - text/html + // responses: + // "200": + // "$ref": "#/responses/MarkupRender" + // "422": + // "$ref": "#/responses/validationError" + + form := web.GetForm(ctx).(*api.MarkupOption) + + if ctx.HasAPIError() { + ctx.Error(http.StatusUnprocessableEntity, "", ctx.GetErrMsg()) + return + } + + common.RenderMarkup(ctx.Context, form.Mode, form.Text, form.Context, form.FilePath, form.Wiki) +} + // Markdown render markdown document to HTML func Markdown(ctx *context.APIContext) { // swagger:operation POST /markdown miscellaneous renderMarkdown @@ -45,55 +71,12 @@ func Markdown(ctx *context.APIContext) { return } - if len(form.Text) == 0 { - _, _ = ctx.Write([]byte("")) - return + mode := "markdown" + if form.Mode == "comment" || form.Mode == "gfm" { + mode = form.Mode } - switch form.Mode { - case "comment": - fallthrough - case "gfm": - urlPrefix := form.Context - 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 { - // "gfm" = Github Flavored Markdown - set this to render as a document - if form.Mode == "gfm" { - meta = ctx.Repo.Repository.ComposeDocumentMetas() - } else { - meta = ctx.Repo.Repository.ComposeMetas() - } - } - if form.Mode == "gfm" { - meta["mode"] = "document" - } - - if err := markdown.Render(&markup.RenderContext{ - Ctx: ctx, - URLPrefix: urlPrefix, - Metas: meta, - IsWiki: form.Wiki, - }, strings.NewReader(form.Text), ctx.Resp); err != nil { - ctx.InternalServerError(err) - return - } - default: - if err := markdown.RenderRaw(&markup.RenderContext{ - Ctx: ctx, - URLPrefix: form.Context, - }, strings.NewReader(form.Text), ctx.Resp); err != nil { - ctx.InternalServerError(err) - return - } - } + common.RenderMarkup(ctx.Context, mode, form.Text, form.Context, "", form.Wiki) } // MarkdownRaw render raw markdown HTML diff --git a/routers/api/v1/misc/markdown_test.go b/routers/api/v1/misc/markup_test.go index 025f2f44b0..301f51eea2 100644 --- a/routers/api/v1/misc/markdown_test.go +++ b/routers/api/v1/misc/markup_test.go @@ -49,16 +49,37 @@ func wrap(ctx *context.Context) *context.APIContext { } } -func TestAPI_RenderGFM(t *testing.T) { +func testRenderMarkup(t *testing.T, mode, filePath, text, responseBody string, responseCode int) { + setting.AppURL = AppURL + + options := api.MarkupOption{ + Mode: mode, + Text: "", + Context: Repo, + Wiki: true, + FilePath: filePath, + } + requrl, _ := url.Parse(util.URLJoin(AppURL, "api", "v1", "markup")) + req := &http.Request{ + Method: "POST", + URL: requrl, + } + m, resp := createContext(req) + ctx := wrap(m) + + options.Text = text + web.SetForm(ctx, &options) + Markup(ctx) + assert.Equal(t, responseBody, resp.Body.String()) + assert.Equal(t, responseCode, resp.Code) + resp.Body.Reset() +} + +func testRenderMarkdown(t *testing.T, mode, text, responseBody string, responseCode int) { setting.AppURL = AppURL - markup.Init(&markup.ProcessorHelper{ - IsUsernameMentionable: func(ctx go_context.Context, username string) bool { - return username == "r-lyeh" - }, - }) options := api.MarkdownOption{ - Mode: "gfm", + Mode: mode, Text: "", Context: Repo, Wiki: true, @@ -71,7 +92,22 @@ func TestAPI_RenderGFM(t *testing.T) { m, resp := createContext(req) ctx := wrap(m) - testCases := []string{ + options.Text = text + web.SetForm(ctx, &options) + Markdown(ctx) + assert.Equal(t, responseBody, resp.Body.String()) + assert.Equal(t, responseCode, resp.Code) + resp.Body.Reset() +} + +func TestAPI_RenderGFM(t *testing.T) { + markup.Init(&markup.ProcessorHelper{ + IsUsernameMentionable: func(ctx go_context.Context, username string) bool { + return username == "r-lyeh" + }, + }) + + testCasesCommon := []string{ // dear imgui wiki markdown extract: special wiki syntax `Wiki! Enjoy :) - [[Links, Language bindings, Engine bindings|Links]] @@ -85,6 +121,23 @@ func TestAPI_RenderGFM(t *testing.T) { <li>Bezier widget (by <a href="` + AppURL + `r-lyeh" rel="nofollow">@r-lyeh</a>) <a href="https://github.com/ocornut/imgui/issues/786" rel="nofollow">https://github.com/ocornut/imgui/issues/786</a></li> </ul> `, + // Guard wiki sidebar: special syntax + `[[Guardfile-DSL / Configuring-Guard|Guardfile-DSL---Configuring-Guard]]`, + // rendered + `<p><a href="` + AppSubURL + `wiki/Guardfile-DSL---Configuring-Guard" rel="nofollow">Guardfile-DSL / Configuring-Guard</a></p> +`, + // special syntax + `[[Name|Link]]`, + // rendered + `<p><a href="` + AppSubURL + `wiki/Link" rel="nofollow">Name</a></p> +`, + // empty + ``, + // rendered + ``, + } + + testCasesDocument := []string{ // wine-staging wiki home extract: special wiki syntax, images `## What is Wine Staging? **Wine Staging** on website [wine-staging.com](http://wine-staging.com). @@ -103,29 +156,28 @@ Here are some links to the most important topics. You can find the full list of <p><a href="` + AppSubURL + `wiki/Configuration" rel="nofollow">Configuration</a> <a href="` + AppSubURL + `wiki/raw/images/icon-bug.png" rel="nofollow"><img src="` + AppSubURL + `wiki/raw/images/icon-bug.png" title="icon-bug.png" alt="images/icon-bug.png"/></a></p> `, - // Guard wiki sidebar: special syntax - `[[Guardfile-DSL / Configuring-Guard|Guardfile-DSL---Configuring-Guard]]`, - // rendered - `<p><a href="` + AppSubURL + `wiki/Guardfile-DSL---Configuring-Guard" rel="nofollow">Guardfile-DSL / Configuring-Guard</a></p> -`, - // special syntax - `[[Name|Link]]`, - // rendered - `<p><a href="` + AppSubURL + `wiki/Link" rel="nofollow">Name</a></p> -`, - // empty - ``, - // rendered - ``, } - for i := 0; i < len(testCases); i += 2 { - options.Text = testCases[i] - web.SetForm(ctx, &options) - Markdown(ctx) - assert.Equal(t, testCases[i+1], resp.Body.String()) - resp.Body.Reset() + for i := 0; i < len(testCasesCommon); i += 2 { + text := testCasesCommon[i] + response := testCasesCommon[i+1] + testRenderMarkdown(t, "gfm", text, response, http.StatusOK) + testRenderMarkup(t, "gfm", "", text, response, http.StatusOK) + testRenderMarkdown(t, "comment", text, response, http.StatusOK) + testRenderMarkup(t, "comment", "", text, response, http.StatusOK) + testRenderMarkup(t, "file", "path/test.md", text, response, http.StatusOK) } + + for i := 0; i < len(testCasesDocument); i += 2 { + text := testCasesDocument[i] + response := testCasesDocument[i+1] + testRenderMarkdown(t, "gfm", text, response, http.StatusOK) + testRenderMarkup(t, "gfm", "", text, response, http.StatusOK) + testRenderMarkup(t, "file", "path/test.md", text, response, http.StatusOK) + } + + testRenderMarkup(t, "file", "path/test.unknown", "## Test", "Unsupported render extension: .unknown\n", http.StatusUnprocessableEntity) + testRenderMarkup(t, "unknown", "", "## Test", "Unknown mode: unknown\n", http.StatusUnprocessableEntity) } var simpleCases = []string{ diff --git a/routers/api/v1/swagger/options.go b/routers/api/v1/swagger/options.go index 0c8d3d353f..1ddc93c383 100644 --- a/routers/api/v1/swagger/options.go +++ b/routers/api/v1/swagger/options.go @@ -57,6 +57,8 @@ type swaggerParameterBodies struct { EditLabelOption api.EditLabelOption // in:body + MarkupOption api.MarkupOption + // in:body MarkdownOption api.MarkdownOption // in:body diff --git a/routers/common/markup.go b/routers/common/markup.go new file mode 100644 index 0000000000..89f24e0007 --- /dev/null +++ b/routers/common/markup.go @@ -0,0 +1,92 @@ +// Copyright 2014 The Gogs Authors. All rights reserved. +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package common + +import ( + "fmt" + "net/http" + "strings" + + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/markup" + "code.gitea.io/gitea/modules/markup/markdown" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" + + "mvdan.cc/xurls/v2" +) + +// RenderMarkup renders markup text for the /markup and /markdown endpoints +func RenderMarkup(ctx *context.Context, mode, text, urlPrefix, filePath string, wiki bool) { + markupType := "" + relativePath := "" + + if len(text) == 0 { + _, _ = ctx.Write([]byte("")) + return + } + + switch mode { + case "markdown": + // Raw markdown + if err := markdown.RenderRaw(&markup.RenderContext{ + Ctx: ctx, + URLPrefix: urlPrefix, + }, strings.NewReader(text), ctx.Resp); err != nil { + ctx.Error(http.StatusInternalServerError, err.Error()) + } + return + case "comment": + // Comment as markdown + markupType = markdown.MarkupName + case "gfm": + // Github Flavored Markdown as document + markupType = markdown.MarkupName + case "file": + // File as document based on file extension + markupType = "" + relativePath = filePath + default: + ctx.Error(http.StatusUnprocessableEntity, fmt.Sprintf("Unknown mode: %s", mode)) + return + } + + 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, urlPrefix) + } + } + + meta := map[string]string{} + if ctx.Repo != nil && ctx.Repo.Repository != nil { + if mode == "comment" { + meta = ctx.Repo.Repository.ComposeMetas() + } else { + meta = ctx.Repo.Repository.ComposeDocumentMetas() + } + } + if mode != "comment" { + meta["mode"] = "document" + } + + if err := markup.Render(&markup.RenderContext{ + Ctx: ctx, + URLPrefix: urlPrefix, + Metas: meta, + IsWiki: wiki, + Type: markupType, + RelativePath: relativePath, + }, strings.NewReader(text), ctx.Resp); err != nil { + if markup.IsErrUnsupportedRenderExtension(err) { + ctx.Error(http.StatusUnprocessableEntity, err.Error()) + } else { + ctx.Error(http.StatusInternalServerError, err.Error()) + } + return + } +} diff --git a/routers/web/misc/markdown.go b/routers/web/misc/markdown.go deleted file mode 100644 index aaa3ed0781..0000000000 --- a/routers/web/misc/markdown.go +++ /dev/null @@ -1,98 +0,0 @@ -// Copyright 2014 The Gogs Authors. All rights reserved. -// Copyright 2022 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package misc - -import ( - "net/http" - "strings" - - "code.gitea.io/gitea/modules/context" - "code.gitea.io/gitea/modules/markup" - "code.gitea.io/gitea/modules/markup/markdown" - "code.gitea.io/gitea/modules/setting" - api "code.gitea.io/gitea/modules/structs" - "code.gitea.io/gitea/modules/util" - "code.gitea.io/gitea/modules/web" - - "mvdan.cc/xurls/v2" -) - -// Markdown render markdown document to HTML -func Markdown(ctx *context.Context) { - // swagger:operation POST /markdown miscellaneous renderMarkdown - // --- - // summary: Render a markdown document as HTML - // parameters: - // - name: body - // in: body - // schema: - // "$ref": "#/definitions/MarkdownOption" - // consumes: - // - application/json - // produces: - // - text/html - // responses: - // "200": - // "$ref": "#/responses/MarkdownRender" - // "422": - // "$ref": "#/responses/validationError" - - form := web.GetForm(ctx).(*api.MarkdownOption) - - if ctx.HasAPIError() { - ctx.Error(http.StatusUnprocessableEntity, "", ctx.GetErrMsg()) - return - } - - if len(form.Text) == 0 { - _, _ = ctx.Write([]byte("")) - return - } - - switch form.Mode { - case "comment": - fallthrough - case "gfm": - urlPrefix := form.Context - 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 { - // "gfm" = Github Flavored Markdown - set this to render as a document - if form.Mode == "gfm" { - meta = ctx.Repo.Repository.ComposeDocumentMetas() - } else { - meta = ctx.Repo.Repository.ComposeMetas() - } - } - if form.Mode == "gfm" { - meta["mode"] = "document" - } - - if err := markdown.Render(&markup.RenderContext{ - Ctx: ctx, - URLPrefix: urlPrefix, - Metas: meta, - IsWiki: form.Wiki, - }, strings.NewReader(form.Text), ctx.Resp); err != nil { - ctx.Error(http.StatusInternalServerError, err.Error()) - return - } - default: - if err := markdown.RenderRaw(&markup.RenderContext{ - Ctx: ctx, - URLPrefix: form.Context, - }, strings.NewReader(form.Text), ctx.Resp); err != nil { - ctx.Error(http.StatusInternalServerError, err.Error()) - return - } - } -} diff --git a/routers/web/misc/markup.go b/routers/web/misc/markup.go new file mode 100644 index 0000000000..f678316f44 --- /dev/null +++ b/routers/web/misc/markup.go @@ -0,0 +1,44 @@ +// Copyright 2014 The Gogs Authors. All rights reserved. +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package misc + +import ( + "net/http" + + "code.gitea.io/gitea/modules/context" + api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/web" + "code.gitea.io/gitea/routers/common" +) + +// Markup render markup document to HTML +func Markup(ctx *context.Context) { + // swagger:operation POST /markup miscellaneous renderMarkup + // --- + // summary: Render a markup document as HTML + // parameters: + // - name: body + // in: body + // schema: + // "$ref": "#/definitions/MarkupOption" + // consumes: + // - application/json + // produces: + // - text/html + // responses: + // "200": + // "$ref": "#/responses/MarkupRender" + // "422": + // "$ref": "#/responses/validationError" + + form := web.GetForm(ctx).(*api.MarkupOption) + + if ctx.HasAPIError() { + ctx.Error(http.StatusUnprocessableEntity, "", ctx.GetErrMsg()) + return + } + + common.RenderMarkup(ctx, form.Mode, form.Text, form.Context, form.FilePath, form.Wiki) +} diff --git a/routers/web/repo/editor.go b/routers/web/repo/editor.go index 07241b8870..2b66be22ae 100644 --- a/routers/web/repo/editor.go +++ b/routers/web/repo/editor.go @@ -20,6 +20,7 @@ import ( "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/typesniffer" "code.gitea.io/gitea/modules/upload" @@ -155,9 +156,8 @@ func editFile(ctx *context.Context, isNewFile bool) { } ctx.Data["new_branch_name"] = GetUniquePatchBranchName(ctx) ctx.Data["last_commit"] = ctx.Repo.CommitID - ctx.Data["MarkdownFileExts"] = strings.Join(setting.Markdown.FileExtensions, ",") + ctx.Data["PreviewableExtensions"] = strings.Join(markup.PreviewableExtensions(), ",") ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",") - ctx.Data["PreviewableFileModes"] = strings.Join(setting.Repository.Editor.PreviewableFileModes, ",") ctx.Data["Editorconfig"] = GetEditorConfig(ctx, treePath) ctx.HTML(http.StatusOK, tplEditFile) @@ -207,9 +207,8 @@ func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile b ctx.Data["commit_choice"] = form.CommitChoice ctx.Data["new_branch_name"] = form.NewBranchName ctx.Data["last_commit"] = ctx.Repo.CommitID - ctx.Data["MarkdownFileExts"] = strings.Join(setting.Markdown.FileExtensions, ",") + ctx.Data["PreviewableExtensions"] = strings.Join(markup.PreviewableExtensions(), ",") ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",") - ctx.Data["PreviewableFileModes"] = strings.Join(setting.Repository.Editor.PreviewableFileModes, ",") ctx.Data["Editorconfig"] = GetEditorConfig(ctx, form.TreePath) if ctx.HasError() { diff --git a/routers/web/web.go b/routers/web/web.go index 292268dc80..4bd2f76c57 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -1115,7 +1115,7 @@ func RegisterRoutes(m *web.Route) { m.Group("/comments/{id}", func() { m.Get("/attachments", repo.GetCommentAttachments) }) - m.Post("/markdown", web.Bind(structs.MarkdownOption{}), misc.Markdown) + m.Post("/markup", web.Bind(structs.MarkupOption{}), misc.Markup) m.Group("/labels", func() { m.Post("/new", web.Bind(forms.CreateLabelForm{}), repo.NewLabel) m.Post("/edit", web.Bind(forms.CreateLabelForm{}), repo.UpdateLabel) |