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>
This commit is contained in:
Brecht Van Lommel 2023-03-24 07:12:23 +01:00 committed by GitHub
parent 9e04627aca
commit 84daddc2fa
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 389 additions and 215 deletions

View file

@ -993,10 +993,6 @@ ROUTER = console
;; List of file extensions for which lines should be wrapped in the Monaco editor ;; 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 ;; Separate extensions with a comma. To line wrap files without an extension, just put a comma
;LINE_WRAP_EXTENSIONS = .txt,.md,.markdown,.mdown,.mkd, ;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
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

View file

@ -283,6 +283,11 @@ type ErrUnsupportedRenderExtension struct {
Extension string Extension string
} }
func IsErrUnsupportedRenderExtension(err error) bool {
_, ok := err.(ErrUnsupportedRenderExtension)
return ok
}
func (err ErrUnsupportedRenderExtension) Error() string { func (err ErrUnsupportedRenderExtension) Error() string {
return fmt.Sprintf("Unsupported render extension: %s", err.Extension) return fmt.Sprintf("Unsupported render extension: %s", err.Extension)
} }
@ -317,3 +322,11 @@ func IsMarkupFile(name, markup string) bool {
} }
return false return false
} }
func PreviewableExtensions() []string {
extensions := make([]string, 0, len(extRenderers))
for extension := range extRenderers {
extensions = append(extensions, extension)
}
return extensions
}

View file

@ -54,7 +54,6 @@ var (
// Repository editor settings // Repository editor settings
Editor struct { Editor struct {
LineWrapExtensions []string LineWrapExtensions []string
PreviewableFileModes []string
} `ini:"-"` } `ini:"-"`
// Repository upload settings // Repository upload settings
@ -168,10 +167,8 @@ var (
// Repository editor settings // Repository editor settings
Editor: struct { Editor: struct {
LineWrapExtensions []string LineWrapExtensions []string
PreviewableFileModes []string
}{ }{
LineWrapExtensions: strings.Split(".txt,.md,.markdown,.mdown,.mkd,", ","), LineWrapExtensions: strings.Split(".txt,.md,.markdown,.mdown,.mkd,", ","),
PreviewableFileModes: []string{"markdown"},
}, },
// Repository upload settings // Repository upload settings

View file

@ -15,13 +15,41 @@ type SearchError struct {
Error string `json:"error"` 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 // MarkdownOption markdown options
type MarkdownOption struct { type MarkdownOption struct {
// Text markdown to render // Text markdown to render
// //
// in: body // in: body
Text string Text string
// Mode to render // Mode to render (comment, gfm, markdown)
// //
// in: body // in: body
Mode string Mode string

View file

@ -711,6 +711,7 @@ func Routes(ctx gocontext.Context) *web.Route {
}) })
} }
m.Get("/signing-key.gpg", misc.SigningKey) 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", bind(api.MarkdownOption{}), misc.Markdown)
m.Post("/markdown/raw", misc.MarkdownRaw) m.Post("/markdown/raw", misc.MarkdownRaw)
m.Group("/settings", func() { 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). 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) 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", reqToken(auth_model.AccessTokenScopeRepo), bind(api.MarkdownOption{}), misc.Markdown)
m.Post("/markdown/raw", reqToken(auth_model.AccessTokenScopeRepo), misc.MarkdownRaw) m.Post("/markdown/raw", reqToken(auth_model.AccessTokenScopeRepo), misc.MarkdownRaw)
m.Group("/milestones", func() { m.Group("/milestones", func() {

View file

@ -5,19 +5,45 @@ package misc
import ( import (
"net/http" "net/http"
"strings"
"code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/markup/markdown" "code.gitea.io/gitea/modules/markup/markdown"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs" api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/common"
"mvdan.cc/xurls/v2"
) )
// 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 // Markdown render markdown document to HTML
func Markdown(ctx *context.APIContext) { func Markdown(ctx *context.APIContext) {
// swagger:operation POST /markdown miscellaneous renderMarkdown // swagger:operation POST /markdown miscellaneous renderMarkdown
@ -45,55 +71,12 @@ func Markdown(ctx *context.APIContext) {
return return
} }
if len(form.Text) == 0 { mode := "markdown"
_, _ = ctx.Write([]byte("")) if form.Mode == "comment" || form.Mode == "gfm" {
return mode = form.Mode
} }
switch form.Mode { common.RenderMarkup(ctx.Context, mode, form.Text, form.Context, "", form.Wiki)
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
}
}
} }
// MarkdownRaw render raw markdown HTML // MarkdownRaw render raw markdown HTML

View file

@ -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 setting.AppURL = AppURL
markup.Init(&markup.ProcessorHelper{
IsUsernameMentionable: func(ctx go_context.Context, username string) bool {
return username == "r-lyeh"
},
})
options := api.MarkdownOption{ options := api.MarkdownOption{
Mode: "gfm", Mode: mode,
Text: "", Text: "",
Context: Repo, Context: Repo,
Wiki: true, Wiki: true,
@ -71,7 +92,22 @@ func TestAPI_RenderGFM(t *testing.T) {
m, resp := createContext(req) m, resp := createContext(req)
ctx := wrap(m) 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 // dear imgui wiki markdown extract: special wiki syntax
`Wiki! Enjoy :) `Wiki! Enjoy :)
- [[Links, Language bindings, Engine bindings|Links]] - [[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> <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> </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 // wine-staging wiki home extract: special wiki syntax, images
`## What is Wine Staging? `## What is Wine Staging?
**Wine Staging** on website [wine-staging.com](http://wine-staging.com). **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> <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> <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 { for i := 0; i < len(testCasesCommon); i += 2 {
options.Text = testCases[i] text := testCasesCommon[i]
web.SetForm(ctx, &options) response := testCasesCommon[i+1]
Markdown(ctx) testRenderMarkdown(t, "gfm", text, response, http.StatusOK)
assert.Equal(t, testCases[i+1], resp.Body.String()) testRenderMarkup(t, "gfm", "", text, response, http.StatusOK)
resp.Body.Reset() 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{ var simpleCases = []string{

View file

@ -56,6 +56,8 @@ type swaggerParameterBodies struct {
// in:body // in:body
EditLabelOption api.EditLabelOption EditLabelOption api.EditLabelOption
// in:body
MarkupOption api.MarkupOption
// in:body // in:body
MarkdownOption api.MarkdownOption MarkdownOption api.MarkdownOption

92
routers/common/markup.go Normal file
View file

@ -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
}
}

View file

@ -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
}
}
}

View file

@ -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)
}

View file

@ -20,6 +20,7 @@ import (
"code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/typesniffer" "code.gitea.io/gitea/modules/typesniffer"
"code.gitea.io/gitea/modules/upload" "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["new_branch_name"] = GetUniquePatchBranchName(ctx)
ctx.Data["last_commit"] = ctx.Repo.CommitID 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["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",")
ctx.Data["PreviewableFileModes"] = strings.Join(setting.Repository.Editor.PreviewableFileModes, ",")
ctx.Data["Editorconfig"] = GetEditorConfig(ctx, treePath) ctx.Data["Editorconfig"] = GetEditorConfig(ctx, treePath)
ctx.HTML(http.StatusOK, tplEditFile) 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["commit_choice"] = form.CommitChoice
ctx.Data["new_branch_name"] = form.NewBranchName ctx.Data["new_branch_name"] = form.NewBranchName
ctx.Data["last_commit"] = ctx.Repo.CommitID 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["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",")
ctx.Data["PreviewableFileModes"] = strings.Join(setting.Repository.Editor.PreviewableFileModes, ",")
ctx.Data["Editorconfig"] = GetEditorConfig(ctx, form.TreePath) ctx.Data["Editorconfig"] = GetEditorConfig(ctx, form.TreePath)
if ctx.HasError() { if ctx.HasError() {

View file

@ -1115,7 +1115,7 @@ func RegisterRoutes(m *web.Route) {
m.Group("/comments/{id}", func() { m.Group("/comments/{id}", func() {
m.Get("/attachments", repo.GetCommentAttachments) 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.Group("/labels", func() {
m.Post("/new", web.Bind(forms.CreateLabelForm{}), repo.NewLabel) m.Post("/new", web.Bind(forms.CreateLabelForm{}), repo.NewLabel)
m.Post("/edit", web.Bind(forms.CreateLabelForm{}), repo.UpdateLabel) m.Post("/edit", web.Bind(forms.CreateLabelForm{}), repo.UpdateLabel)

View file

@ -192,7 +192,7 @@
<div class="ui comment form"> <div class="ui comment form">
<div class="ui top attached tabular menu"> <div class="ui top attached tabular menu">
<a class="active write item">{{$.locale.Tr "write"}}</a> <a class="active write item">{{$.locale.Tr "write"}}</a>
<a class="preview item" data-url="{{$.Repository.Link}}/markdown" data-context="{{$.RepoLink}}">{{$.locale.Tr "preview"}}</a> <a class="preview item" data-url="{{$.Repository.Link}}/markup" data-context="{{$.RepoLink}}">{{$.locale.Tr "preview"}}</a>
</div> </div>
<div class="ui bottom attached active write tab segment"> <div class="ui bottom attached active write tab segment">
<textarea class="review-textarea js-quick-submit" tabindex="1" name="content"></textarea> <textarea class="review-textarea js-quick-submit" tabindex="1" name="content"></textarea>

View file

@ -11,7 +11,7 @@
<input type="hidden" name="diff_base_cid"> <input type="hidden" name="diff_base_cid">
<div class="ui top tabular menu" data-write="write" data-preview="preview"> <div class="ui top tabular menu" data-write="write" data-preview="preview">
<a class="active item" data-tab="write">{{$.root.locale.Tr "write"}}</a> <a class="active item" data-tab="write">{{$.root.locale.Tr "write"}}</a>
<a class="item" data-tab="preview" data-url="{{$.root.Repository.Link}}/markdown" data-context="{{$.root.RepoLink}}">{{$.root.locale.Tr "preview"}}</a> <a class="item" data-tab="preview" data-url="{{$.root.Repository.Link}}/markup" data-context="{{$.root.RepoLink}}">{{$.root.locale.Tr "preview"}}</a>
</div> </div>
<div class="field"> <div class="field">
<div class="ui active tab" data-tab="write"> <div class="ui active tab" data-tab="write">

View file

@ -31,15 +31,15 @@
<div class="ui top attached tabular menu" data-write="write" data-preview="preview" data-diff="diff"> <div class="ui top attached tabular menu" data-write="write" data-preview="preview" data-diff="diff">
<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> <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>
{{if not .IsNewFile}} {{if not .IsNewFile}}
<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> <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>
<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> <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>
{{end}} {{end}}
</div> </div>
<div class="ui bottom attached active tab segment" data-tab="write"> <div class="ui bottom attached active tab segment" data-tab="write">
<textarea id="edit_area" name="content" class="gt-hidden" data-id="repo-{{.Repository.Name}}-{{.TreePath}}" <textarea id="edit_area" name="content" class="gt-hidden" data-id="repo-{{.Repository.Name}}-{{.TreePath}}"
data-url="{{.Repository.Link}}/markdown" data-url="{{.Repository.Link}}/markup"
data-context="{{.RepoLink}}" data-context="{{.RepoLink}}"
data-markdown-file-exts="{{.MarkdownFileExts}}" data-previewable-extensions="{{.PreviewableExtensions}}"
data-line-wrap-extensions="{{.LineWrapExtensions}}"> data-line-wrap-extensions="{{.LineWrapExtensions}}">
{{.FileContent}}</textarea> {{.FileContent}}</textarea>
<div class="editor-loading is-loading"></div> <div class="editor-loading is-loading"></div>

View file

@ -1,10 +1,10 @@
<div class="ui top tabular menu" data-write="write" data-preview="preview"> <div class="ui top tabular menu" data-write="write" data-preview="preview">
<a class="active item" data-tab="write">{{.locale.Tr "write"}}</a> <a class="active item" data-tab="write">{{.locale.Tr "write"}}</a>
<a class="item" data-tab="preview" data-url="{{.Repository.Link}}/markdown" data-context="{{.RepoLink}}">{{.locale.Tr "preview"}}</a> <a class="item" data-tab="preview" data-url="{{.Repository.Link}}/markup" data-context="{{.RepoLink}}">{{.locale.Tr "preview"}}</a>
</div> </div>
<div class="field"> <div class="field">
<div class="ui bottom active tab" data-tab="write"> <div class="ui bottom active tab" data-tab="write">
<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}}"> <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}}">
{{- if .BodyQuery}}{{.BodyQuery}}{{else if .IssueTemplate}}{{.IssueTemplate}}{{else if .PullRequestTemplate}}{{.PullRequestTemplate}}{{else}}{{.content}}{{end -}} {{- if .BodyQuery}}{{.BodyQuery}}{{else if .IssueTemplate}}{{.IssueTemplate}}{{else if .PullRequestTemplate}}{{.PullRequestTemplate}}{{else}}{{.content}}{{end -}}
</textarea> </textarea>
</div> </div>

View file

@ -168,7 +168,7 @@
<div class="ui comment form"> <div class="ui comment form">
<div class="ui top tabular menu"> <div class="ui top tabular menu">
<a class="active write item">{{$.locale.Tr "write"}}</a> <a class="active write item">{{$.locale.Tr "write"}}</a>
<a class="preview item" data-url="{{$.Repository.Link}}/markdown" data-context="{{$.RepoLink}}">{{$.locale.Tr "preview"}}</a> <a class="preview item" data-url="{{$.Repository.Link}}/markup" data-context="{{$.RepoLink}}">{{$.locale.Tr "preview"}}</a>
</div> </div>
<div class="field"> <div class="field">
<div class="ui bottom active tab write"> <div class="ui bottom active tab write">

View file

@ -53,7 +53,7 @@
<label>{{.locale.Tr "repo.release.content"}}</label> <label>{{.locale.Tr "repo.release.content"}}</label>
<div class="ui top tabular menu" data-write="write" data-preview="preview"> <div class="ui top tabular menu" data-write="write" data-preview="preview">
<a class="active write item" data-tab="write">{{$.locale.Tr "write"}}</a> <a class="active write item" data-tab="write">{{$.locale.Tr "write"}}</a>
<a class="preview item" data-tab="preview" data-url="{{$.Repository.Link}}/markdown" data-context="{{$.RepoLink}}">{{$.locale.Tr "preview"}}</a> <a class="preview item" data-tab="preview" data-url="{{$.Repository.Link}}/markup" data-context="{{$.RepoLink}}">{{$.locale.Tr "preview"}}</a>
</div> </div>
<div class="ui bottom active tab" data-tab="write"> <div class="ui bottom active tab" data-tab="write">
<textarea name="content">{{.content}}</textarea> <textarea name="content">{{.content}}</textarea>

View file

@ -21,11 +21,11 @@
</div> </div>
<div class="ui top attached tabular menu previewtabs" data-write="write" data-preview="preview"> <div class="ui top attached tabular menu previewtabs" data-write="write" data-preview="preview">
<a class="active item" data-tab="write">{{.locale.Tr "write"}}</a> <a class="active item" data-tab="write">{{.locale.Tr "write"}}</a>
<a class="item" data-tab="preview" data-url="{{$.Repository.Link}}/markdown" data-context="{{$.RepoLink}}">{{$.locale.Tr "preview"}}</a> <a class="item" data-tab="preview" data-url="{{$.Repository.Link}}/markup" data-context="{{$.RepoLink}}">{{$.locale.Tr "preview"}}</a>
</div> </div>
<div class="field content" data-loading="{{.locale.Tr "loading"}}"> <div class="field content" data-loading="{{.locale.Tr "loading"}}">
<div class="ui bottom active tab" data-tab="write"> <div class="ui bottom active tab" data-tab="write">
<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> <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>
</div> </div>
</div> </div>
<div class="field"> <div class="field">

View file

@ -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": { "/nodeinfo": {
"get": { "get": {
"produces": [ "produces": [
@ -17991,7 +18023,7 @@
"type": "string" "type": "string"
}, },
"Mode": { "Mode": {
"description": "Mode to render\n\nin: body", "description": "Mode to render (comment, gfm, markdown)\n\nin: body",
"type": "string" "type": "string"
}, },
"Text": { "Text": {
@ -18005,6 +18037,33 @@
}, },
"x-go-package": "code.gitea.io/gitea/modules/structs" "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": { "MergePullRequestOption": {
"description": "MergePullRequestForm form for merging Pull Request", "description": "MergePullRequestForm form for merging Pull Request",
"type": "object", "type": "object",
@ -20835,6 +20894,12 @@
"type": "string" "type": "string"
} }
}, },
"MarkupRender": {
"description": "MarkupRender is a rendered markup document",
"schema": {
"type": "string"
}
},
"Milestone": { "Milestone": {
"description": "Milestone", "description": "Milestone",
"schema": { "schema": {

View file

@ -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 filename = basename(filenameInput.value);
const previewLink = document.querySelector('a[data-tab=preview]'); 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 lineWrapExts = (textarea.getAttribute('data-line-wrap-extensions') || '').split(',');
const isMarkdown = markdownExts.includes(extname(filename)); const previewable = previewableExts.includes(extname(filename));
const editorConfig = getEditorconfig(filenameInput); const editorConfig = getEditorconfig(filenameInput);
if (previewLink) { if (previewLink) {
if (isMarkdown && (previewFileModes || []).includes('markdown')) { if (previewable) {
const newUrl = (previewLink.getAttribute('data-url') || '').replace(/(.*)\/.*/i, `$1/markdown`); const newUrl = (previewLink.getAttribute('data-url') || '').replace(/(.*)\/.*/i, `$1/markup`);
previewLink.setAttribute('data-url', newUrl); previewLink.setAttribute('data-url', newUrl);
previewLink.style.display = ''; previewLink.style.display = '';
} else { } else {

View file

@ -5,18 +5,16 @@ import {createCodeEditor} from './codeeditor.js';
import {hideElem, showElem} from '../utils/dom.js'; import {hideElem, showElem} from '../utils/dom.js';
const {csrfToken} = window.config; const {csrfToken} = window.config;
let previewFileModes;
function initEditPreviewTab($form) { function initEditPreviewTab($form) {
const $tabMenu = $form.find('.tabular.menu'); const $tabMenu = $form.find('.tabular.menu');
$tabMenu.find('.item').tab(); $tabMenu.find('.item').tab();
const $previewTab = $tabMenu.find(`.item[data-tab="${$tabMenu.data('preview')}"]`); const $previewTab = $tabMenu.find(`.item[data-tab="${$tabMenu.data('preview')}"]`);
if ($previewTab.length) { if ($previewTab.length) {
previewFileModes = $previewTab.data('preview-file-modes').split(',');
$previewTab.on('click', function () { $previewTab.on('click', function () {
const $this = $(this); const $this = $(this);
let context = `${$this.data('context')}/`; 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'); const treePathEl = $form.find('input#tree_path');
if (treePathEl.length > 0) { if (treePathEl.length > 0) {
context += treePathEl.val(); context += treePathEl.val();
@ -27,6 +25,7 @@ function initEditPreviewTab($form) {
mode, mode,
context, context,
text: $form.find(`.tab[data-tab="${$tabMenu.data('write')}"] textarea`).val(), text: $form.find(`.tab[data-tab="${$tabMenu.data('write')}"] textarea`).val(),
file_path: treePathEl.val(),
}, (data) => { }, (data) => {
const $previewPanel = $form.find(`.tab[data-tab="${$tabMenu.data('preview')}"]`); const $previewPanel = $form.find(`.tab[data-tab="${$tabMenu.data('preview')}"]`);
$previewPanel.html(data); $previewPanel.html(data);
@ -147,7 +146,7 @@ export function initRepoEditor() {
if (!$editArea.length) return; if (!$editArea.length) return;
(async () => { (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 // Using events from https://github.com/codedance/jquery.AreYouSure#advanced-usage
// to enable or disable the commit button // to enable or disable the commit button