From 84daddc2fa74393cdc13371b0cc44f0444cfdae0 Mon Sep 17 00:00:00 2001 From: Brecht Van Lommel Date: Fri, 24 Mar 2023 07:12:23 +0100 Subject: [PATCH] Editor preview support for external renderers (#23333) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Co-authored-by: Lunny Xiao --- custom/conf/app.example.ini | 4 - modules/markup/renderer.go | 13 +++ modules/setting/repository.go | 9 +- modules/structs/miscellaneous.go | 30 ++++- routers/api/v1/api.go | 2 + .../api/v1/misc/{markdown.go => markup.go} | 87 ++++++-------- .../misc/{markdown_test.go => markup_test.go} | 108 +++++++++++++----- routers/api/v1/swagger/options.go | 2 + routers/common/markup.go | 92 +++++++++++++++ routers/web/misc/markdown.go | 98 ---------------- routers/web/misc/markup.go | 44 +++++++ routers/web/repo/editor.go | 7 +- routers/web/web.go | 2 +- templates/repo/diff/box.tmpl | 2 +- templates/repo/diff/comment_form.tmpl | 2 +- templates/repo/editor/edit.tmpl | 6 +- templates/repo/issue/comment_tab.tmpl | 4 +- templates/repo/issue/view_content.tmpl | 2 +- templates/repo/release/new.tmpl | 2 +- templates/repo/wiki/new.tmpl | 4 +- templates/swagger/v1_json.tmpl | 67 ++++++++++- web_src/js/features/codeeditor.js | 10 +- web_src/js/features/repo-editor.js | 7 +- 23 files changed, 389 insertions(+), 215 deletions(-) rename routers/api/v1/misc/{markdown.go => markup.go} (56%) rename routers/api/v1/misc/{markdown_test.go => markup_test.go} (71%) create mode 100644 routers/common/markup.go delete mode 100644 routers/web/misc/markdown.go create mode 100644 routers/web/misc/markup.go diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index 131fb3401e..d1cfcd70e5 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -993,10 +993,6 @@ ROUTER = console ;; List of file extensions for which lines should be wrapped in the Monaco editor ;; Separate extensions with a comma. To line wrap files without an extension, just put a comma ;LINE_WRAP_EXTENSIONS = .txt,.md,.markdown,.mdown,.mkd, -;; -;; Valid file modes that have a preview API associated with them, such as api/v1/markdown -;; Separate the values by commas. The preview tab in edit mode won't be displayed if the file extension doesn't match -;PREVIEWABLE_FILE_MODES = markdown ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/modules/markup/renderer.go b/modules/markup/renderer.go index df2c9ebfc6..e934aed925 100644 --- a/modules/markup/renderer.go +++ b/modules/markup/renderer.go @@ -283,6 +283,11 @@ type ErrUnsupportedRenderExtension struct { Extension string } +func IsErrUnsupportedRenderExtension(err error) bool { + _, ok := err.(ErrUnsupportedRenderExtension) + return ok +} + func (err ErrUnsupportedRenderExtension) Error() string { return fmt.Sprintf("Unsupported render extension: %s", err.Extension) } @@ -317,3 +322,11 @@ func IsMarkupFile(name, markup string) bool { } return false } + +func PreviewableExtensions() []string { + extensions := make([]string, 0, len(extRenderers)) + for extension := range extRenderers { + extensions = append(extensions, extension) + } + return extensions +} diff --git a/modules/setting/repository.go b/modules/setting/repository.go index 4964704dba..bae3c658a4 100644 --- a/modules/setting/repository.go +++ b/modules/setting/repository.go @@ -53,8 +53,7 @@ var ( // Repository editor settings Editor struct { - LineWrapExtensions []string - PreviewableFileModes []string + LineWrapExtensions []string } `ini:"-"` // Repository upload settings @@ -167,11 +166,9 @@ var ( // Repository editor settings Editor: struct { - LineWrapExtensions []string - PreviewableFileModes []string + LineWrapExtensions []string }{ - LineWrapExtensions: strings.Split(".txt,.md,.markdown,.mdown,.mkd,", ","), - PreviewableFileModes: []string{"markdown"}, + LineWrapExtensions: strings.Split(".txt,.md,.markdown,.mdown,.mkd,", ","), }, // Repository upload settings diff --git a/modules/structs/miscellaneous.go b/modules/structs/miscellaneous.go index 596a551e0d..8acea84d6c 100644 --- a/modules/structs/miscellaneous.go +++ b/modules/structs/miscellaneous.go @@ -15,13 +15,41 @@ type SearchError struct { Error string `json:"error"` } +// MarkupOption markup options +type MarkupOption struct { + // Text markup to render + // + // in: body + Text string + // Mode to render (comment, gfm, markdown, file) + // + // in: body + Mode string + // Context to render + // + // in: body + Context string + // Is it a wiki page ? + // + // in: body + Wiki bool + // File path for detecting extension in file mode + // + // in: body + FilePath string +} + +// MarkupRender is a rendered markup document +// swagger:response MarkupRender +type MarkupRender string + // MarkdownOption markdown options type MarkdownOption struct { // Text markdown to render // // in: body Text string - // Mode to render + // Mode to render (comment, gfm, markdown) // // in: body Mode string 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 similarity index 56% rename from routers/api/v1/misc/markdown.go rename to 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 similarity index 71% rename from routers/api/v1/misc/markdown_test.go rename to 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) {
  • Bezier widget (by @r-lyeh) https://github.com/ocornut/imgui/issues/786
  • `, + // Guard wiki sidebar: special syntax + `[[Guardfile-DSL / Configuring-Guard|Guardfile-DSL---Configuring-Guard]]`, + // rendered + `

    Guardfile-DSL / Configuring-Guard

    +`, + // special syntax + `[[Name|Link]]`, + // rendered + `

    Name

    +`, + // 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

    Configuration images/icon-bug.png

    `, - // Guard wiki sidebar: special syntax - `[[Guardfile-DSL / Configuring-Guard|Guardfile-DSL---Configuring-Guard]]`, - // rendered - `

    Guardfile-DSL / Configuring-Guard

    -`, - // special syntax - `[[Name|Link]]`, - // rendered - `

    Name

    -`, - // 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 @@ -56,6 +56,8 @@ type swaggerParameterBodies struct { // in:body EditLabelOption api.EditLabelOption + // in:body + MarkupOption api.MarkupOption // in:body MarkdownOption api.MarkdownOption 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) diff --git a/templates/repo/diff/box.tmpl b/templates/repo/diff/box.tmpl index 6dafe6f896..8926c518ae 100644 --- a/templates/repo/diff/box.tmpl +++ b/templates/repo/diff/box.tmpl @@ -192,7 +192,7 @@
    diff --git a/templates/repo/diff/comment_form.tmpl b/templates/repo/diff/comment_form.tmpl index 225e40f7f0..deea6baffb 100644 --- a/templates/repo/diff/comment_form.tmpl +++ b/templates/repo/diff/comment_form.tmpl @@ -11,7 +11,7 @@
    diff --git a/templates/repo/editor/edit.tmpl b/templates/repo/editor/edit.tmpl index 19c5cb007a..737027f590 100644 --- a/templates/repo/editor/edit.tmpl +++ b/templates/repo/editor/edit.tmpl @@ -31,15 +31,15 @@
    diff --git a/templates/repo/issue/comment_tab.tmpl b/templates/repo/issue/comment_tab.tmpl index b8e8d2d9aa..47d6ca9587 100644 --- a/templates/repo/issue/comment_tab.tmpl +++ b/templates/repo/issue/comment_tab.tmpl @@ -1,10 +1,10 @@
    -
    diff --git a/templates/repo/issue/view_content.tmpl b/templates/repo/issue/view_content.tmpl index 3e43701b63..8a55d4e30e 100644 --- a/templates/repo/issue/view_content.tmpl +++ b/templates/repo/issue/view_content.tmpl @@ -168,7 +168,7 @@
    diff --git a/templates/repo/release/new.tmpl b/templates/repo/release/new.tmpl index 8c4df98d19..692a373dc6 100644 --- a/templates/repo/release/new.tmpl +++ b/templates/repo/release/new.tmpl @@ -53,7 +53,7 @@
    diff --git a/templates/repo/wiki/new.tmpl b/templates/repo/wiki/new.tmpl index 9e9de99022..085af4cbc9 100644 --- a/templates/repo/wiki/new.tmpl +++ b/templates/repo/wiki/new.tmpl @@ -21,11 +21,11 @@
    - +
    diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index ddcdc94b81..0f7e60c598 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -951,6 +951,38 @@ } } }, + "/markup": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "text/html" + ], + "tags": [ + "miscellaneous" + ], + "summary": "Render a markup document as HTML", + "operationId": "renderMarkup", + "parameters": [ + { + "name": "body", + "in": "body", + "schema": { + "$ref": "#/definitions/MarkupOption" + } + } + ], + "responses": { + "200": { + "$ref": "#/responses/MarkupRender" + }, + "422": { + "$ref": "#/responses/validationError" + } + } + } + }, "/nodeinfo": { "get": { "produces": [ @@ -17991,7 +18023,7 @@ "type": "string" }, "Mode": { - "description": "Mode to render\n\nin: body", + "description": "Mode to render (comment, gfm, markdown)\n\nin: body", "type": "string" }, "Text": { @@ -18005,6 +18037,33 @@ }, "x-go-package": "code.gitea.io/gitea/modules/structs" }, + "MarkupOption": { + "description": "MarkupOption markup options", + "type": "object", + "properties": { + "Context": { + "description": "Context to render\n\nin: body", + "type": "string" + }, + "FilePath": { + "description": "File path for detecting extension in file mode\n\nin: body", + "type": "string" + }, + "Mode": { + "description": "Mode to render (comment, gfm, markdown, file)\n\nin: body", + "type": "string" + }, + "Text": { + "description": "Text markup to render\n\nin: body", + "type": "string" + }, + "Wiki": { + "description": "Is it a wiki page ?\n\nin: body", + "type": "boolean" + } + }, + "x-go-package": "code.gitea.io/gitea/modules/structs" + }, "MergePullRequestOption": { "description": "MergePullRequestForm form for merging Pull Request", "type": "object", @@ -20835,6 +20894,12 @@ "type": "string" } }, + "MarkupRender": { + "description": "MarkupRender is a rendered markup document", + "schema": { + "type": "string" + } + }, "Milestone": { "description": "Milestone", "schema": { diff --git a/web_src/js/features/codeeditor.js b/web_src/js/features/codeeditor.js index 23a26ba2b0..4f5ea317b4 100644 --- a/web_src/js/features/codeeditor.js +++ b/web_src/js/features/codeeditor.js @@ -130,17 +130,17 @@ function getFileBasedOptions(filename, lineWrapExts) { }; } -export async function createCodeEditor(textarea, filenameInput, previewFileModes) { +export async function createCodeEditor(textarea, filenameInput) { const filename = basename(filenameInput.value); const previewLink = document.querySelector('a[data-tab=preview]'); - const markdownExts = (textarea.getAttribute('data-markdown-file-exts') || '').split(','); + const previewableExts = (textarea.getAttribute('data-previewable-extensions') || '').split(','); const lineWrapExts = (textarea.getAttribute('data-line-wrap-extensions') || '').split(','); - const isMarkdown = markdownExts.includes(extname(filename)); + const previewable = previewableExts.includes(extname(filename)); const editorConfig = getEditorconfig(filenameInput); if (previewLink) { - if (isMarkdown && (previewFileModes || []).includes('markdown')) { - const newUrl = (previewLink.getAttribute('data-url') || '').replace(/(.*)\/.*/i, `$1/markdown`); + if (previewable) { + const newUrl = (previewLink.getAttribute('data-url') || '').replace(/(.*)\/.*/i, `$1/markup`); previewLink.setAttribute('data-url', newUrl); previewLink.style.display = ''; } else { diff --git a/web_src/js/features/repo-editor.js b/web_src/js/features/repo-editor.js index b3e9b65f21..a7c59fb039 100644 --- a/web_src/js/features/repo-editor.js +++ b/web_src/js/features/repo-editor.js @@ -5,18 +5,16 @@ import {createCodeEditor} from './codeeditor.js'; import {hideElem, showElem} from '../utils/dom.js'; const {csrfToken} = window.config; -let previewFileModes; function initEditPreviewTab($form) { const $tabMenu = $form.find('.tabular.menu'); $tabMenu.find('.item').tab(); const $previewTab = $tabMenu.find(`.item[data-tab="${$tabMenu.data('preview')}"]`); if ($previewTab.length) { - previewFileModes = $previewTab.data('preview-file-modes').split(','); $previewTab.on('click', function () { const $this = $(this); let context = `${$this.data('context')}/`; - const mode = $this.data('markdown-mode') || 'comment'; + const mode = $this.data('markup-mode') || 'comment'; const treePathEl = $form.find('input#tree_path'); if (treePathEl.length > 0) { context += treePathEl.val(); @@ -27,6 +25,7 @@ function initEditPreviewTab($form) { mode, context, text: $form.find(`.tab[data-tab="${$tabMenu.data('write')}"] textarea`).val(), + file_path: treePathEl.val(), }, (data) => { const $previewPanel = $form.find(`.tab[data-tab="${$tabMenu.data('preview')}"]`); $previewPanel.html(data); @@ -147,7 +146,7 @@ export function initRepoEditor() { if (!$editArea.length) return; (async () => { - const editor = await createCodeEditor($editArea[0], $editFilename[0], previewFileModes); + const editor = await createCodeEditor($editArea[0], $editFilename[0]); // Using events from https://github.com/codedance/jquery.AreYouSure#advanced-usage // to enable or disable the commit button