loading up the forgejo repo on tangled to test page performance
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

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>

authored by

Brecht Van Lommel
zeripath
Lunny Xiao
and committed by
GitHub
84daddc2 9e04627a

+464 -290
-4
custom/conf/app.example.ini
··· 993 993 ;; List of file extensions for which lines should be wrapped in the Monaco editor 994 994 ;; Separate extensions with a comma. To line wrap files without an extension, just put a comma 995 995 ;LINE_WRAP_EXTENSIONS = .txt,.md,.markdown,.mdown,.mkd, 996 - ;; 997 - ;; Valid file modes that have a preview API associated with them, such as api/v1/markdown 998 - ;; Separate the values by commas. The preview tab in edit mode won't be displayed if the file extension doesn't match 999 - ;PREVIEWABLE_FILE_MODES = markdown 1000 996 1001 997 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 1002 998 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+13
modules/markup/renderer.go
··· 283 283 Extension string 284 284 } 285 285 286 + func IsErrUnsupportedRenderExtension(err error) bool { 287 + _, ok := err.(ErrUnsupportedRenderExtension) 288 + return ok 289 + } 290 + 286 291 func (err ErrUnsupportedRenderExtension) Error() string { 287 292 return fmt.Sprintf("Unsupported render extension: %s", err.Extension) 288 293 } ··· 317 322 } 318 323 return false 319 324 } 325 + 326 + func PreviewableExtensions() []string { 327 + extensions := make([]string, 0, len(extRenderers)) 328 + for extension := range extRenderers { 329 + extensions = append(extensions, extension) 330 + } 331 + return extensions 332 + }
+3 -6
modules/setting/repository.go
··· 53 53 54 54 // Repository editor settings 55 55 Editor struct { 56 - LineWrapExtensions []string 57 - PreviewableFileModes []string 56 + LineWrapExtensions []string 58 57 } `ini:"-"` 59 58 60 59 // Repository upload settings ··· 167 166 168 167 // Repository editor settings 169 168 Editor: struct { 170 - LineWrapExtensions []string 171 - PreviewableFileModes []string 169 + LineWrapExtensions []string 172 170 }{ 173 - LineWrapExtensions: strings.Split(".txt,.md,.markdown,.mdown,.mkd,", ","), 174 - PreviewableFileModes: []string{"markdown"}, 171 + LineWrapExtensions: strings.Split(".txt,.md,.markdown,.mdown,.mkd,", ","), 175 172 }, 176 173 177 174 // Repository upload settings
+29 -1
modules/structs/miscellaneous.go
··· 15 15 Error string `json:"error"` 16 16 } 17 17 18 + // MarkupOption markup options 19 + type MarkupOption struct { 20 + // Text markup to render 21 + // 22 + // in: body 23 + Text string 24 + // Mode to render (comment, gfm, markdown, file) 25 + // 26 + // in: body 27 + Mode string 28 + // Context to render 29 + // 30 + // in: body 31 + Context string 32 + // Is it a wiki page ? 33 + // 34 + // in: body 35 + Wiki bool 36 + // File path for detecting extension in file mode 37 + // 38 + // in: body 39 + FilePath string 40 + } 41 + 42 + // MarkupRender is a rendered markup document 43 + // swagger:response MarkupRender 44 + type MarkupRender string 45 + 18 46 // MarkdownOption markdown options 19 47 type MarkdownOption struct { 20 48 // Text markdown to render 21 49 // 22 50 // in: body 23 51 Text string 24 - // Mode to render 52 + // Mode to render (comment, gfm, markdown) 25 53 // 26 54 // in: body 27 55 Mode string
+2
routers/api/v1/api.go
··· 711 711 }) 712 712 } 713 713 m.Get("/signing-key.gpg", misc.SigningKey) 714 + m.Post("/markup", bind(api.MarkupOption{}), misc.Markup) 714 715 m.Post("/markdown", bind(api.MarkdownOption{}), misc.Markdown) 715 716 m.Post("/markdown/raw", misc.MarkdownRaw) 716 717 m.Group("/settings", func() { ··· 1034 1035 Patch(reqToken(auth_model.AccessTokenScopeRepo), reqRepoWriter(unit.TypeIssues, unit.TypePullRequests), bind(api.EditLabelOption{}), repo.EditLabel). 1035 1036 Delete(reqToken(auth_model.AccessTokenScopeRepo), reqRepoWriter(unit.TypeIssues, unit.TypePullRequests), repo.DeleteLabel) 1036 1037 }) 1038 + m.Post("/markup", reqToken(auth_model.AccessTokenScopeRepo), bind(api.MarkupOption{}), misc.Markup) 1037 1039 m.Post("/markdown", reqToken(auth_model.AccessTokenScopeRepo), bind(api.MarkdownOption{}), misc.Markdown) 1038 1040 m.Post("/markdown/raw", reqToken(auth_model.AccessTokenScopeRepo), misc.MarkdownRaw) 1039 1041 m.Group("/milestones", func() {
-127
routers/api/v1/misc/markdown.go
··· 1 - // Copyright 2014 The Gogs Authors. All rights reserved. 2 - // SPDX-License-Identifier: MIT 3 - 4 - package misc 5 - 6 - import ( 7 - "net/http" 8 - "strings" 9 - 10 - "code.gitea.io/gitea/modules/context" 11 - "code.gitea.io/gitea/modules/markup" 12 - "code.gitea.io/gitea/modules/markup/markdown" 13 - "code.gitea.io/gitea/modules/setting" 14 - api "code.gitea.io/gitea/modules/structs" 15 - "code.gitea.io/gitea/modules/util" 16 - "code.gitea.io/gitea/modules/web" 17 - 18 - "mvdan.cc/xurls/v2" 19 - ) 20 - 21 - // Markdown render markdown document to HTML 22 - func Markdown(ctx *context.APIContext) { 23 - // swagger:operation POST /markdown miscellaneous renderMarkdown 24 - // --- 25 - // summary: Render a markdown document as HTML 26 - // parameters: 27 - // - name: body 28 - // in: body 29 - // schema: 30 - // "$ref": "#/definitions/MarkdownOption" 31 - // consumes: 32 - // - application/json 33 - // produces: 34 - // - text/html 35 - // responses: 36 - // "200": 37 - // "$ref": "#/responses/MarkdownRender" 38 - // "422": 39 - // "$ref": "#/responses/validationError" 40 - 41 - form := web.GetForm(ctx).(*api.MarkdownOption) 42 - 43 - if ctx.HasAPIError() { 44 - ctx.Error(http.StatusUnprocessableEntity, "", ctx.GetErrMsg()) 45 - return 46 - } 47 - 48 - if len(form.Text) == 0 { 49 - _, _ = ctx.Write([]byte("")) 50 - return 51 - } 52 - 53 - switch form.Mode { 54 - case "comment": 55 - fallthrough 56 - case "gfm": 57 - urlPrefix := form.Context 58 - meta := map[string]string{} 59 - if !strings.HasPrefix(setting.AppSubURL+"/", urlPrefix) { 60 - // check if urlPrefix is already set to a URL 61 - linkRegex, _ := xurls.StrictMatchingScheme("https?://") 62 - m := linkRegex.FindStringIndex(urlPrefix) 63 - if m == nil { 64 - urlPrefix = util.URLJoin(setting.AppURL, form.Context) 65 - } 66 - } 67 - if ctx.Repo != nil && ctx.Repo.Repository != nil { 68 - // "gfm" = Github Flavored Markdown - set this to render as a document 69 - if form.Mode == "gfm" { 70 - meta = ctx.Repo.Repository.ComposeDocumentMetas() 71 - } else { 72 - meta = ctx.Repo.Repository.ComposeMetas() 73 - } 74 - } 75 - if form.Mode == "gfm" { 76 - meta["mode"] = "document" 77 - } 78 - 79 - if err := markdown.Render(&markup.RenderContext{ 80 - Ctx: ctx, 81 - URLPrefix: urlPrefix, 82 - Metas: meta, 83 - IsWiki: form.Wiki, 84 - }, strings.NewReader(form.Text), ctx.Resp); err != nil { 85 - ctx.InternalServerError(err) 86 - return 87 - } 88 - default: 89 - if err := markdown.RenderRaw(&markup.RenderContext{ 90 - Ctx: ctx, 91 - URLPrefix: form.Context, 92 - }, strings.NewReader(form.Text), ctx.Resp); err != nil { 93 - ctx.InternalServerError(err) 94 - return 95 - } 96 - } 97 - } 98 - 99 - // MarkdownRaw render raw markdown HTML 100 - func MarkdownRaw(ctx *context.APIContext) { 101 - // swagger:operation POST /markdown/raw miscellaneous renderMarkdownRaw 102 - // --- 103 - // summary: Render raw markdown as HTML 104 - // parameters: 105 - // - name: body 106 - // in: body 107 - // description: Request body to render 108 - // required: true 109 - // schema: 110 - // type: string 111 - // consumes: 112 - // - text/plain 113 - // produces: 114 - // - text/html 115 - // responses: 116 - // "200": 117 - // "$ref": "#/responses/MarkdownRender" 118 - // "422": 119 - // "$ref": "#/responses/validationError" 120 - defer ctx.Req.Body.Close() 121 - if err := markdown.RenderRaw(&markup.RenderContext{ 122 - Ctx: ctx, 123 - }, ctx.Req.Body, ctx.Resp); err != nil { 124 - ctx.InternalServerError(err) 125 - return 126 - } 127 - }
+80 -28
routers/api/v1/misc/markdown_test.go routers/api/v1/misc/markup_test.go
··· 49 49 } 50 50 } 51 51 52 - func TestAPI_RenderGFM(t *testing.T) { 52 + func testRenderMarkup(t *testing.T, mode, filePath, text, responseBody string, responseCode int) { 53 53 setting.AppURL = AppURL 54 - markup.Init(&markup.ProcessorHelper{ 55 - IsUsernameMentionable: func(ctx go_context.Context, username string) bool { 56 - return username == "r-lyeh" 57 - }, 58 - }) 54 + 55 + options := api.MarkupOption{ 56 + Mode: mode, 57 + Text: "", 58 + Context: Repo, 59 + Wiki: true, 60 + FilePath: filePath, 61 + } 62 + requrl, _ := url.Parse(util.URLJoin(AppURL, "api", "v1", "markup")) 63 + req := &http.Request{ 64 + Method: "POST", 65 + URL: requrl, 66 + } 67 + m, resp := createContext(req) 68 + ctx := wrap(m) 69 + 70 + options.Text = text 71 + web.SetForm(ctx, &options) 72 + Markup(ctx) 73 + assert.Equal(t, responseBody, resp.Body.String()) 74 + assert.Equal(t, responseCode, resp.Code) 75 + resp.Body.Reset() 76 + } 77 + 78 + func testRenderMarkdown(t *testing.T, mode, text, responseBody string, responseCode int) { 79 + setting.AppURL = AppURL 59 80 60 81 options := api.MarkdownOption{ 61 - Mode: "gfm", 82 + Mode: mode, 62 83 Text: "", 63 84 Context: Repo, 64 85 Wiki: true, ··· 71 92 m, resp := createContext(req) 72 93 ctx := wrap(m) 73 94 74 - testCases := []string{ 95 + options.Text = text 96 + web.SetForm(ctx, &options) 97 + Markdown(ctx) 98 + assert.Equal(t, responseBody, resp.Body.String()) 99 + assert.Equal(t, responseCode, resp.Code) 100 + resp.Body.Reset() 101 + } 102 + 103 + func TestAPI_RenderGFM(t *testing.T) { 104 + markup.Init(&markup.ProcessorHelper{ 105 + IsUsernameMentionable: func(ctx go_context.Context, username string) bool { 106 + return username == "r-lyeh" 107 + }, 108 + }) 109 + 110 + testCasesCommon := []string{ 75 111 // dear imgui wiki markdown extract: special wiki syntax 76 112 `Wiki! Enjoy :) 77 113 - [[Links, Language bindings, Engine bindings|Links]] ··· 85 121 <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> 86 122 </ul> 87 123 `, 124 + // Guard wiki sidebar: special syntax 125 + `[[Guardfile-DSL / Configuring-Guard|Guardfile-DSL---Configuring-Guard]]`, 126 + // rendered 127 + `<p><a href="` + AppSubURL + `wiki/Guardfile-DSL---Configuring-Guard" rel="nofollow">Guardfile-DSL / Configuring-Guard</a></p> 128 + `, 129 + // special syntax 130 + `[[Name|Link]]`, 131 + // rendered 132 + `<p><a href="` + AppSubURL + `wiki/Link" rel="nofollow">Name</a></p> 133 + `, 134 + // empty 135 + ``, 136 + // rendered 137 + ``, 138 + } 139 + 140 + testCasesDocument := []string{ 88 141 // wine-staging wiki home extract: special wiki syntax, images 89 142 `## What is Wine Staging? 90 143 **Wine Staging** on website [wine-staging.com](http://wine-staging.com). ··· 103 156 <p><a href="` + AppSubURL + `wiki/Configuration" rel="nofollow">Configuration</a> 104 157 <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> 105 158 `, 106 - // Guard wiki sidebar: special syntax 107 - `[[Guardfile-DSL / Configuring-Guard|Guardfile-DSL---Configuring-Guard]]`, 108 - // rendered 109 - `<p><a href="` + AppSubURL + `wiki/Guardfile-DSL---Configuring-Guard" rel="nofollow">Guardfile-DSL / Configuring-Guard</a></p> 110 - `, 111 - // special syntax 112 - `[[Name|Link]]`, 113 - // rendered 114 - `<p><a href="` + AppSubURL + `wiki/Link" rel="nofollow">Name</a></p> 115 - `, 116 - // empty 117 - ``, 118 - // rendered 119 - ``, 120 159 } 121 160 122 - for i := 0; i < len(testCases); i += 2 { 123 - options.Text = testCases[i] 124 - web.SetForm(ctx, &options) 125 - Markdown(ctx) 126 - assert.Equal(t, testCases[i+1], resp.Body.String()) 127 - resp.Body.Reset() 161 + for i := 0; i < len(testCasesCommon); i += 2 { 162 + text := testCasesCommon[i] 163 + response := testCasesCommon[i+1] 164 + testRenderMarkdown(t, "gfm", text, response, http.StatusOK) 165 + testRenderMarkup(t, "gfm", "", text, response, http.StatusOK) 166 + testRenderMarkdown(t, "comment", text, response, http.StatusOK) 167 + testRenderMarkup(t, "comment", "", text, response, http.StatusOK) 168 + testRenderMarkup(t, "file", "path/test.md", text, response, http.StatusOK) 169 + } 170 + 171 + for i := 0; i < len(testCasesDocument); i += 2 { 172 + text := testCasesDocument[i] 173 + response := testCasesDocument[i+1] 174 + testRenderMarkdown(t, "gfm", text, response, http.StatusOK) 175 + testRenderMarkup(t, "gfm", "", text, response, http.StatusOK) 176 + testRenderMarkup(t, "file", "path/test.md", text, response, http.StatusOK) 128 177 } 178 + 179 + testRenderMarkup(t, "file", "path/test.unknown", "## Test", "Unsupported render extension: .unknown\n", http.StatusUnprocessableEntity) 180 + testRenderMarkup(t, "unknown", "", "## Test", "Unknown mode: unknown\n", http.StatusUnprocessableEntity) 129 181 } 130 182 131 183 var simpleCases = []string{
+110
routers/api/v1/misc/markup.go
··· 1 + // Copyright 2014 The Gogs Authors. All rights reserved. 2 + // SPDX-License-Identifier: MIT 3 + 4 + package misc 5 + 6 + import ( 7 + "net/http" 8 + 9 + "code.gitea.io/gitea/modules/context" 10 + "code.gitea.io/gitea/modules/markup" 11 + "code.gitea.io/gitea/modules/markup/markdown" 12 + api "code.gitea.io/gitea/modules/structs" 13 + "code.gitea.io/gitea/modules/web" 14 + "code.gitea.io/gitea/routers/common" 15 + ) 16 + 17 + // Markup render markup document to HTML 18 + func Markup(ctx *context.APIContext) { 19 + // swagger:operation POST /markup miscellaneous renderMarkup 20 + // --- 21 + // summary: Render a markup document as HTML 22 + // parameters: 23 + // - name: body 24 + // in: body 25 + // schema: 26 + // "$ref": "#/definitions/MarkupOption" 27 + // consumes: 28 + // - application/json 29 + // produces: 30 + // - text/html 31 + // responses: 32 + // "200": 33 + // "$ref": "#/responses/MarkupRender" 34 + // "422": 35 + // "$ref": "#/responses/validationError" 36 + 37 + form := web.GetForm(ctx).(*api.MarkupOption) 38 + 39 + if ctx.HasAPIError() { 40 + ctx.Error(http.StatusUnprocessableEntity, "", ctx.GetErrMsg()) 41 + return 42 + } 43 + 44 + common.RenderMarkup(ctx.Context, form.Mode, form.Text, form.Context, form.FilePath, form.Wiki) 45 + } 46 + 47 + // Markdown render markdown document to HTML 48 + func Markdown(ctx *context.APIContext) { 49 + // swagger:operation POST /markdown miscellaneous renderMarkdown 50 + // --- 51 + // summary: Render a markdown document as HTML 52 + // parameters: 53 + // - name: body 54 + // in: body 55 + // schema: 56 + // "$ref": "#/definitions/MarkdownOption" 57 + // consumes: 58 + // - application/json 59 + // produces: 60 + // - text/html 61 + // responses: 62 + // "200": 63 + // "$ref": "#/responses/MarkdownRender" 64 + // "422": 65 + // "$ref": "#/responses/validationError" 66 + 67 + form := web.GetForm(ctx).(*api.MarkdownOption) 68 + 69 + if ctx.HasAPIError() { 70 + ctx.Error(http.StatusUnprocessableEntity, "", ctx.GetErrMsg()) 71 + return 72 + } 73 + 74 + mode := "markdown" 75 + if form.Mode == "comment" || form.Mode == "gfm" { 76 + mode = form.Mode 77 + } 78 + 79 + common.RenderMarkup(ctx.Context, mode, form.Text, form.Context, "", form.Wiki) 80 + } 81 + 82 + // MarkdownRaw render raw markdown HTML 83 + func MarkdownRaw(ctx *context.APIContext) { 84 + // swagger:operation POST /markdown/raw miscellaneous renderMarkdownRaw 85 + // --- 86 + // summary: Render raw markdown as HTML 87 + // parameters: 88 + // - name: body 89 + // in: body 90 + // description: Request body to render 91 + // required: true 92 + // schema: 93 + // type: string 94 + // consumes: 95 + // - text/plain 96 + // produces: 97 + // - text/html 98 + // responses: 99 + // "200": 100 + // "$ref": "#/responses/MarkdownRender" 101 + // "422": 102 + // "$ref": "#/responses/validationError" 103 + defer ctx.Req.Body.Close() 104 + if err := markdown.RenderRaw(&markup.RenderContext{ 105 + Ctx: ctx, 106 + }, ctx.Req.Body, ctx.Resp); err != nil { 107 + ctx.InternalServerError(err) 108 + return 109 + } 110 + }
+2
routers/api/v1/swagger/options.go
··· 57 57 EditLabelOption api.EditLabelOption 58 58 59 59 // in:body 60 + MarkupOption api.MarkupOption 61 + // in:body 60 62 MarkdownOption api.MarkdownOption 61 63 62 64 // in:body
+92
routers/common/markup.go
··· 1 + // Copyright 2014 The Gogs Authors. All rights reserved. 2 + // Copyright 2023 The Gitea Authors. All rights reserved. 3 + // SPDX-License-Identifier: MIT 4 + 5 + package common 6 + 7 + import ( 8 + "fmt" 9 + "net/http" 10 + "strings" 11 + 12 + "code.gitea.io/gitea/modules/context" 13 + "code.gitea.io/gitea/modules/markup" 14 + "code.gitea.io/gitea/modules/markup/markdown" 15 + "code.gitea.io/gitea/modules/setting" 16 + "code.gitea.io/gitea/modules/util" 17 + 18 + "mvdan.cc/xurls/v2" 19 + ) 20 + 21 + // RenderMarkup renders markup text for the /markup and /markdown endpoints 22 + func RenderMarkup(ctx *context.Context, mode, text, urlPrefix, filePath string, wiki bool) { 23 + markupType := "" 24 + relativePath := "" 25 + 26 + if len(text) == 0 { 27 + _, _ = ctx.Write([]byte("")) 28 + return 29 + } 30 + 31 + switch mode { 32 + case "markdown": 33 + // Raw markdown 34 + if err := markdown.RenderRaw(&markup.RenderContext{ 35 + Ctx: ctx, 36 + URLPrefix: urlPrefix, 37 + }, strings.NewReader(text), ctx.Resp); err != nil { 38 + ctx.Error(http.StatusInternalServerError, err.Error()) 39 + } 40 + return 41 + case "comment": 42 + // Comment as markdown 43 + markupType = markdown.MarkupName 44 + case "gfm": 45 + // Github Flavored Markdown as document 46 + markupType = markdown.MarkupName 47 + case "file": 48 + // File as document based on file extension 49 + markupType = "" 50 + relativePath = filePath 51 + default: 52 + ctx.Error(http.StatusUnprocessableEntity, fmt.Sprintf("Unknown mode: %s", mode)) 53 + return 54 + } 55 + 56 + if !strings.HasPrefix(setting.AppSubURL+"/", urlPrefix) { 57 + // check if urlPrefix is already set to a URL 58 + linkRegex, _ := xurls.StrictMatchingScheme("https?://") 59 + m := linkRegex.FindStringIndex(urlPrefix) 60 + if m == nil { 61 + urlPrefix = util.URLJoin(setting.AppURL, urlPrefix) 62 + } 63 + } 64 + 65 + meta := map[string]string{} 66 + if ctx.Repo != nil && ctx.Repo.Repository != nil { 67 + if mode == "comment" { 68 + meta = ctx.Repo.Repository.ComposeMetas() 69 + } else { 70 + meta = ctx.Repo.Repository.ComposeDocumentMetas() 71 + } 72 + } 73 + if mode != "comment" { 74 + meta["mode"] = "document" 75 + } 76 + 77 + if err := markup.Render(&markup.RenderContext{ 78 + Ctx: ctx, 79 + URLPrefix: urlPrefix, 80 + Metas: meta, 81 + IsWiki: wiki, 82 + Type: markupType, 83 + RelativePath: relativePath, 84 + }, strings.NewReader(text), ctx.Resp); err != nil { 85 + if markup.IsErrUnsupportedRenderExtension(err) { 86 + ctx.Error(http.StatusUnprocessableEntity, err.Error()) 87 + } else { 88 + ctx.Error(http.StatusInternalServerError, err.Error()) 89 + } 90 + return 91 + } 92 + }
-98
routers/web/misc/markdown.go
··· 1 - // Copyright 2014 The Gogs Authors. All rights reserved. 2 - // Copyright 2022 The Gitea Authors. All rights reserved. 3 - // SPDX-License-Identifier: MIT 4 - 5 - package misc 6 - 7 - import ( 8 - "net/http" 9 - "strings" 10 - 11 - "code.gitea.io/gitea/modules/context" 12 - "code.gitea.io/gitea/modules/markup" 13 - "code.gitea.io/gitea/modules/markup/markdown" 14 - "code.gitea.io/gitea/modules/setting" 15 - api "code.gitea.io/gitea/modules/structs" 16 - "code.gitea.io/gitea/modules/util" 17 - "code.gitea.io/gitea/modules/web" 18 - 19 - "mvdan.cc/xurls/v2" 20 - ) 21 - 22 - // Markdown render markdown document to HTML 23 - func Markdown(ctx *context.Context) { 24 - // swagger:operation POST /markdown miscellaneous renderMarkdown 25 - // --- 26 - // summary: Render a markdown document as HTML 27 - // parameters: 28 - // - name: body 29 - // in: body 30 - // schema: 31 - // "$ref": "#/definitions/MarkdownOption" 32 - // consumes: 33 - // - application/json 34 - // produces: 35 - // - text/html 36 - // responses: 37 - // "200": 38 - // "$ref": "#/responses/MarkdownRender" 39 - // "422": 40 - // "$ref": "#/responses/validationError" 41 - 42 - form := web.GetForm(ctx).(*api.MarkdownOption) 43 - 44 - if ctx.HasAPIError() { 45 - ctx.Error(http.StatusUnprocessableEntity, "", ctx.GetErrMsg()) 46 - return 47 - } 48 - 49 - if len(form.Text) == 0 { 50 - _, _ = ctx.Write([]byte("")) 51 - return 52 - } 53 - 54 - switch form.Mode { 55 - case "comment": 56 - fallthrough 57 - case "gfm": 58 - urlPrefix := form.Context 59 - meta := map[string]string{} 60 - if !strings.HasPrefix(setting.AppSubURL+"/", urlPrefix) { 61 - // check if urlPrefix is already set to a URL 62 - linkRegex, _ := xurls.StrictMatchingScheme("https?://") 63 - m := linkRegex.FindStringIndex(urlPrefix) 64 - if m == nil { 65 - urlPrefix = util.URLJoin(setting.AppURL, form.Context) 66 - } 67 - } 68 - if ctx.Repo != nil && ctx.Repo.Repository != nil { 69 - // "gfm" = Github Flavored Markdown - set this to render as a document 70 - if form.Mode == "gfm" { 71 - meta = ctx.Repo.Repository.ComposeDocumentMetas() 72 - } else { 73 - meta = ctx.Repo.Repository.ComposeMetas() 74 - } 75 - } 76 - if form.Mode == "gfm" { 77 - meta["mode"] = "document" 78 - } 79 - 80 - if err := markdown.Render(&markup.RenderContext{ 81 - Ctx: ctx, 82 - URLPrefix: urlPrefix, 83 - Metas: meta, 84 - IsWiki: form.Wiki, 85 - }, strings.NewReader(form.Text), ctx.Resp); err != nil { 86 - ctx.Error(http.StatusInternalServerError, err.Error()) 87 - return 88 - } 89 - default: 90 - if err := markdown.RenderRaw(&markup.RenderContext{ 91 - Ctx: ctx, 92 - URLPrefix: form.Context, 93 - }, strings.NewReader(form.Text), ctx.Resp); err != nil { 94 - ctx.Error(http.StatusInternalServerError, err.Error()) 95 - return 96 - } 97 - } 98 - }
+44
routers/web/misc/markup.go
··· 1 + // Copyright 2014 The Gogs Authors. All rights reserved. 2 + // Copyright 2022 The Gitea Authors. All rights reserved. 3 + // SPDX-License-Identifier: MIT 4 + 5 + package misc 6 + 7 + import ( 8 + "net/http" 9 + 10 + "code.gitea.io/gitea/modules/context" 11 + api "code.gitea.io/gitea/modules/structs" 12 + "code.gitea.io/gitea/modules/web" 13 + "code.gitea.io/gitea/routers/common" 14 + ) 15 + 16 + // Markup render markup document to HTML 17 + func Markup(ctx *context.Context) { 18 + // swagger:operation POST /markup miscellaneous renderMarkup 19 + // --- 20 + // summary: Render a markup document as HTML 21 + // parameters: 22 + // - name: body 23 + // in: body 24 + // schema: 25 + // "$ref": "#/definitions/MarkupOption" 26 + // consumes: 27 + // - application/json 28 + // produces: 29 + // - text/html 30 + // responses: 31 + // "200": 32 + // "$ref": "#/responses/MarkupRender" 33 + // "422": 34 + // "$ref": "#/responses/validationError" 35 + 36 + form := web.GetForm(ctx).(*api.MarkupOption) 37 + 38 + if ctx.HasAPIError() { 39 + ctx.Error(http.StatusUnprocessableEntity, "", ctx.GetErrMsg()) 40 + return 41 + } 42 + 43 + common.RenderMarkup(ctx, form.Mode, form.Text, form.Context, form.FilePath, form.Wiki) 44 + }
+3 -4
routers/web/repo/editor.go
··· 20 20 "code.gitea.io/gitea/modules/git" 21 21 "code.gitea.io/gitea/modules/json" 22 22 "code.gitea.io/gitea/modules/log" 23 + "code.gitea.io/gitea/modules/markup" 23 24 "code.gitea.io/gitea/modules/setting" 24 25 "code.gitea.io/gitea/modules/typesniffer" 25 26 "code.gitea.io/gitea/modules/upload" ··· 155 156 } 156 157 ctx.Data["new_branch_name"] = GetUniquePatchBranchName(ctx) 157 158 ctx.Data["last_commit"] = ctx.Repo.CommitID 158 - ctx.Data["MarkdownFileExts"] = strings.Join(setting.Markdown.FileExtensions, ",") 159 + ctx.Data["PreviewableExtensions"] = strings.Join(markup.PreviewableExtensions(), ",") 159 160 ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",") 160 - ctx.Data["PreviewableFileModes"] = strings.Join(setting.Repository.Editor.PreviewableFileModes, ",") 161 161 ctx.Data["Editorconfig"] = GetEditorConfig(ctx, treePath) 162 162 163 163 ctx.HTML(http.StatusOK, tplEditFile) ··· 207 207 ctx.Data["commit_choice"] = form.CommitChoice 208 208 ctx.Data["new_branch_name"] = form.NewBranchName 209 209 ctx.Data["last_commit"] = ctx.Repo.CommitID 210 - ctx.Data["MarkdownFileExts"] = strings.Join(setting.Markdown.FileExtensions, ",") 210 + ctx.Data["PreviewableExtensions"] = strings.Join(markup.PreviewableExtensions(), ",") 211 211 ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",") 212 - ctx.Data["PreviewableFileModes"] = strings.Join(setting.Repository.Editor.PreviewableFileModes, ",") 213 212 ctx.Data["Editorconfig"] = GetEditorConfig(ctx, form.TreePath) 214 213 215 214 if ctx.HasError() {
+1 -1
routers/web/web.go
··· 1115 1115 m.Group("/comments/{id}", func() { 1116 1116 m.Get("/attachments", repo.GetCommentAttachments) 1117 1117 }) 1118 - m.Post("/markdown", web.Bind(structs.MarkdownOption{}), misc.Markdown) 1118 + m.Post("/markup", web.Bind(structs.MarkupOption{}), misc.Markup) 1119 1119 m.Group("/labels", func() { 1120 1120 m.Post("/new", web.Bind(forms.CreateLabelForm{}), repo.NewLabel) 1121 1121 m.Post("/edit", web.Bind(forms.CreateLabelForm{}), repo.UpdateLabel)
+1 -1
templates/repo/diff/box.tmpl
··· 192 192 <div class="ui comment form"> 193 193 <div class="ui top attached tabular menu"> 194 194 <a class="active write item">{{$.locale.Tr "write"}}</a> 195 - <a class="preview item" data-url="{{$.Repository.Link}}/markdown" data-context="{{$.RepoLink}}">{{$.locale.Tr "preview"}}</a> 195 + <a class="preview item" data-url="{{$.Repository.Link}}/markup" data-context="{{$.RepoLink}}">{{$.locale.Tr "preview"}}</a> 196 196 </div> 197 197 <div class="ui bottom attached active write tab segment"> 198 198 <textarea class="review-textarea js-quick-submit" tabindex="1" name="content"></textarea>
+1 -1
templates/repo/diff/comment_form.tmpl
··· 11 11 <input type="hidden" name="diff_base_cid"> 12 12 <div class="ui top tabular menu" data-write="write" data-preview="preview"> 13 13 <a class="active item" data-tab="write">{{$.root.locale.Tr "write"}}</a> 14 - <a class="item" data-tab="preview" data-url="{{$.root.Repository.Link}}/markdown" data-context="{{$.root.RepoLink}}">{{$.root.locale.Tr "preview"}}</a> 14 + <a class="item" data-tab="preview" data-url="{{$.root.Repository.Link}}/markup" data-context="{{$.root.RepoLink}}">{{$.root.locale.Tr "preview"}}</a> 15 15 </div> 16 16 <div class="field"> 17 17 <div class="ui active tab" data-tab="write">
+3 -3
templates/repo/editor/edit.tmpl
··· 31 31 <div class="ui top attached tabular menu" data-write="write" data-preview="preview" data-diff="diff"> 32 32 <a class="active item" data-tab="write">{{svg "octicon-code"}} {{if .IsNewFile}}{{.locale.Tr "repo.editor.new_file"}}{{else}}{{.locale.Tr "repo.editor.edit_file"}}{{end}}</a> 33 33 {{if not .IsNewFile}} 34 - <a class="item" data-tab="preview" data-url="{{.Repository.Link}}/markdown" data-context="{{.RepoLink}}/src/{{.BranchNameSubURL}}" data-preview-file-modes="{{.PreviewableFileModes}}" data-markdown-mode="gfm">{{svg "octicon-eye"}} {{.locale.Tr "preview"}}</a> 34 + <a class="item" data-tab="preview" data-url="{{.Repository.Link}}/markup" data-context="{{.RepoLink}}/src/{{.BranchNameSubURL}}" data-markup-mode="file">{{svg "octicon-eye"}} {{.locale.Tr "preview"}}</a> 35 35 <a class="item" data-tab="diff" data-url="{{.RepoLink}}/_preview/{{.BranchName | PathEscapeSegments}}/{{.TreePath | PathEscapeSegments}}" data-context="{{.BranchLink}}">{{svg "octicon-diff"}} {{.locale.Tr "repo.editor.preview_changes"}}</a> 36 36 {{end}} 37 37 </div> 38 38 <div class="ui bottom attached active tab segment" data-tab="write"> 39 39 <textarea id="edit_area" name="content" class="gt-hidden" data-id="repo-{{.Repository.Name}}-{{.TreePath}}" 40 - data-url="{{.Repository.Link}}/markdown" 40 + data-url="{{.Repository.Link}}/markup" 41 41 data-context="{{.RepoLink}}" 42 - data-markdown-file-exts="{{.MarkdownFileExts}}" 42 + data-previewable-extensions="{{.PreviewableExtensions}}" 43 43 data-line-wrap-extensions="{{.LineWrapExtensions}}"> 44 44 {{.FileContent}}</textarea> 45 45 <div class="editor-loading is-loading"></div>
+2 -2
templates/repo/issue/comment_tab.tmpl
··· 1 1 <div class="ui top tabular menu" data-write="write" data-preview="preview"> 2 2 <a class="active item" data-tab="write">{{.locale.Tr "write"}}</a> 3 - <a class="item" data-tab="preview" data-url="{{.Repository.Link}}/markdown" data-context="{{.RepoLink}}">{{.locale.Tr "preview"}}</a> 3 + <a class="item" data-tab="preview" data-url="{{.Repository.Link}}/markup" data-context="{{.RepoLink}}">{{.locale.Tr "preview"}}</a> 4 4 </div> 5 5 <div class="field"> 6 6 <div class="ui bottom active tab" data-tab="write"> 7 - <textarea id="content" class="edit_area js-quick-submit" name="content" tabindex="4" data-id="issue-{{.RepoName}}" data-url="{{.Repository.Link}}/markdown" data-context="{{.Repo.RepoLink}}"> 7 + <textarea id="content" class="edit_area js-quick-submit" name="content" tabindex="4" data-id="issue-{{.RepoName}}" data-url="{{.Repository.Link}}/markup" data-context="{{.Repo.RepoLink}}"> 8 8 {{- if .BodyQuery}}{{.BodyQuery}}{{else if .IssueTemplate}}{{.IssueTemplate}}{{else if .PullRequestTemplate}}{{.PullRequestTemplate}}{{else}}{{.content}}{{end -}} 9 9 </textarea> 10 10 </div>
+1 -1
templates/repo/issue/view_content.tmpl
··· 168 168 <div class="ui comment form"> 169 169 <div class="ui top tabular menu"> 170 170 <a class="active write item">{{$.locale.Tr "write"}}</a> 171 - <a class="preview item" data-url="{{$.Repository.Link}}/markdown" data-context="{{$.RepoLink}}">{{$.locale.Tr "preview"}}</a> 171 + <a class="preview item" data-url="{{$.Repository.Link}}/markup" data-context="{{$.RepoLink}}">{{$.locale.Tr "preview"}}</a> 172 172 </div> 173 173 <div class="field"> 174 174 <div class="ui bottom active tab write">
+1 -1
templates/repo/release/new.tmpl
··· 53 53 <label>{{.locale.Tr "repo.release.content"}}</label> 54 54 <div class="ui top tabular menu" data-write="write" data-preview="preview"> 55 55 <a class="active write item" data-tab="write">{{$.locale.Tr "write"}}</a> 56 - <a class="preview item" data-tab="preview" data-url="{{$.Repository.Link}}/markdown" data-context="{{$.RepoLink}}">{{$.locale.Tr "preview"}}</a> 56 + <a class="preview item" data-tab="preview" data-url="{{$.Repository.Link}}/markup" data-context="{{$.RepoLink}}">{{$.locale.Tr "preview"}}</a> 57 57 </div> 58 58 <div class="ui bottom active tab" data-tab="write"> 59 59 <textarea name="content">{{.content}}</textarea>
+2 -2
templates/repo/wiki/new.tmpl
··· 21 21 </div> 22 22 <div class="ui top attached tabular menu previewtabs" data-write="write" data-preview="preview"> 23 23 <a class="active item" data-tab="write">{{.locale.Tr "write"}}</a> 24 - <a class="item" data-tab="preview" data-url="{{$.Repository.Link}}/markdown" data-context="{{$.RepoLink}}">{{$.locale.Tr "preview"}}</a> 24 + <a class="item" data-tab="preview" data-url="{{$.Repository.Link}}/markup" data-context="{{$.RepoLink}}">{{$.locale.Tr "preview"}}</a> 25 25 </div> 26 26 <div class="field content" data-loading="{{.locale.Tr "loading"}}"> 27 27 <div class="ui bottom active tab" data-tab="write"> 28 - <textarea class="js-quick-submit" id="edit_area" name="content" data-id="wiki-{{.title}}" data-url="{{.Repository.Link}}/markdown" data-context="{{.RepoLink}}">{{if .PageIsWikiEdit}}{{.content}}{{else}}{{.locale.Tr "repo.wiki.welcome"}}{{end}}</textarea> 28 + <textarea class="js-quick-submit" id="edit_area" name="content" data-id="wiki-{{.title}}" data-url="{{.Repository.Link}}/markup" data-context="{{.RepoLink}}">{{if .PageIsWikiEdit}}{{.content}}{{else}}{{.locale.Tr "repo.wiki.welcome"}}{{end}}</textarea> 29 29 </div> 30 30 </div> 31 31 <div class="field">
+66 -1
templates/swagger/v1_json.tmpl
··· 951 951 } 952 952 } 953 953 }, 954 + "/markup": { 955 + "post": { 956 + "consumes": [ 957 + "application/json" 958 + ], 959 + "produces": [ 960 + "text/html" 961 + ], 962 + "tags": [ 963 + "miscellaneous" 964 + ], 965 + "summary": "Render a markup document as HTML", 966 + "operationId": "renderMarkup", 967 + "parameters": [ 968 + { 969 + "name": "body", 970 + "in": "body", 971 + "schema": { 972 + "$ref": "#/definitions/MarkupOption" 973 + } 974 + } 975 + ], 976 + "responses": { 977 + "200": { 978 + "$ref": "#/responses/MarkupRender" 979 + }, 980 + "422": { 981 + "$ref": "#/responses/validationError" 982 + } 983 + } 984 + } 985 + }, 954 986 "/nodeinfo": { 955 987 "get": { 956 988 "produces": [ ··· 17991 18023 "type": "string" 17992 18024 }, 17993 18025 "Mode": { 17994 - "description": "Mode to render\n\nin: body", 18026 + "description": "Mode to render (comment, gfm, markdown)\n\nin: body", 17995 18027 "type": "string" 17996 18028 }, 17997 18029 "Text": { 17998 18030 "description": "Text markdown to render\n\nin: body", 18031 + "type": "string" 18032 + }, 18033 + "Wiki": { 18034 + "description": "Is it a wiki page ?\n\nin: body", 18035 + "type": "boolean" 18036 + } 18037 + }, 18038 + "x-go-package": "code.gitea.io/gitea/modules/structs" 18039 + }, 18040 + "MarkupOption": { 18041 + "description": "MarkupOption markup options", 18042 + "type": "object", 18043 + "properties": { 18044 + "Context": { 18045 + "description": "Context to render\n\nin: body", 18046 + "type": "string" 18047 + }, 18048 + "FilePath": { 18049 + "description": "File path for detecting extension in file mode\n\nin: body", 18050 + "type": "string" 18051 + }, 18052 + "Mode": { 18053 + "description": "Mode to render (comment, gfm, markdown, file)\n\nin: body", 18054 + "type": "string" 18055 + }, 18056 + "Text": { 18057 + "description": "Text markup to render\n\nin: body", 17999 18058 "type": "string" 18000 18059 }, 18001 18060 "Wiki": { ··· 20831 20890 }, 20832 20891 "MarkdownRender": { 20833 20892 "description": "MarkdownRender is a rendered markdown document", 20893 + "schema": { 20894 + "type": "string" 20895 + } 20896 + }, 20897 + "MarkupRender": { 20898 + "description": "MarkupRender is a rendered markup document", 20834 20899 "schema": { 20835 20900 "type": "string" 20836 20901 }
+5 -5
web_src/js/features/codeeditor.js
··· 130 130 }; 131 131 } 132 132 133 - export async function createCodeEditor(textarea, filenameInput, previewFileModes) { 133 + export async function createCodeEditor(textarea, filenameInput) { 134 134 const filename = basename(filenameInput.value); 135 135 const previewLink = document.querySelector('a[data-tab=preview]'); 136 - const markdownExts = (textarea.getAttribute('data-markdown-file-exts') || '').split(','); 136 + const previewableExts = (textarea.getAttribute('data-previewable-extensions') || '').split(','); 137 137 const lineWrapExts = (textarea.getAttribute('data-line-wrap-extensions') || '').split(','); 138 - const isMarkdown = markdownExts.includes(extname(filename)); 138 + const previewable = previewableExts.includes(extname(filename)); 139 139 const editorConfig = getEditorconfig(filenameInput); 140 140 141 141 if (previewLink) { 142 - if (isMarkdown && (previewFileModes || []).includes('markdown')) { 143 - const newUrl = (previewLink.getAttribute('data-url') || '').replace(/(.*)\/.*/i, `$1/markdown`); 142 + if (previewable) { 143 + const newUrl = (previewLink.getAttribute('data-url') || '').replace(/(.*)\/.*/i, `$1/markup`); 144 144 previewLink.setAttribute('data-url', newUrl); 145 145 previewLink.style.display = ''; 146 146 } else {
+3 -4
web_src/js/features/repo-editor.js
··· 5 5 import {hideElem, showElem} from '../utils/dom.js'; 6 6 7 7 const {csrfToken} = window.config; 8 - let previewFileModes; 9 8 10 9 function initEditPreviewTab($form) { 11 10 const $tabMenu = $form.find('.tabular.menu'); 12 11 $tabMenu.find('.item').tab(); 13 12 const $previewTab = $tabMenu.find(`.item[data-tab="${$tabMenu.data('preview')}"]`); 14 13 if ($previewTab.length) { 15 - previewFileModes = $previewTab.data('preview-file-modes').split(','); 16 14 $previewTab.on('click', function () { 17 15 const $this = $(this); 18 16 let context = `${$this.data('context')}/`; 19 - const mode = $this.data('markdown-mode') || 'comment'; 17 + const mode = $this.data('markup-mode') || 'comment'; 20 18 const treePathEl = $form.find('input#tree_path'); 21 19 if (treePathEl.length > 0) { 22 20 context += treePathEl.val(); ··· 27 25 mode, 28 26 context, 29 27 text: $form.find(`.tab[data-tab="${$tabMenu.data('write')}"] textarea`).val(), 28 + file_path: treePathEl.val(), 30 29 }, (data) => { 31 30 const $previewPanel = $form.find(`.tab[data-tab="${$tabMenu.data('preview')}"]`); 32 31 $previewPanel.html(data); ··· 147 146 if (!$editArea.length) return; 148 147 149 148 (async () => { 150 - const editor = await createCodeEditor($editArea[0], $editFilename[0], previewFileModes); 149 + const editor = await createCodeEditor($editArea[0], $editFilename[0]); 151 150 152 151 // Using events from https://github.com/codedance/jquery.AreYouSure#advanced-usage 153 152 // to enable or disable the commit button