mirror of
https://codeberg.org/forgejo/forgejo
synced 2024-11-22 01:44:24 +01:00
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:
parent
9e04627aca
commit
84daddc2fa
|
@ -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
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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
|
|
@ -49,16 +49,37 @@ func wrap(ctx *context.Context) *context.APIContext {
|
|||
}
|
||||
}
|
||||
|
||||
func TestAPI_RenderGFM(t *testing.T) {
|
||||
func testRenderMarkup(t *testing.T, mode, filePath, text, responseBody string, responseCode int) {
|
||||
setting.AppURL = AppURL
|
||||
|
||||
options := api.MarkupOption{
|
||||
Mode: mode,
|
||||
Text: "",
|
||||
Context: Repo,
|
||||
Wiki: true,
|
||||
FilePath: filePath,
|
||||
}
|
||||
requrl, _ := url.Parse(util.URLJoin(AppURL, "api", "v1", "markup"))
|
||||
req := &http.Request{
|
||||
Method: "POST",
|
||||
URL: requrl,
|
||||
}
|
||||
m, resp := createContext(req)
|
||||
ctx := wrap(m)
|
||||
|
||||
options.Text = text
|
||||
web.SetForm(ctx, &options)
|
||||
Markup(ctx)
|
||||
assert.Equal(t, responseBody, resp.Body.String())
|
||||
assert.Equal(t, responseCode, resp.Code)
|
||||
resp.Body.Reset()
|
||||
}
|
||||
|
||||
func testRenderMarkdown(t *testing.T, mode, text, responseBody string, responseCode int) {
|
||||
setting.AppURL = AppURL
|
||||
markup.Init(&markup.ProcessorHelper{
|
||||
IsUsernameMentionable: func(ctx go_context.Context, username string) bool {
|
||||
return username == "r-lyeh"
|
||||
},
|
||||
})
|
||||
|
||||
options := api.MarkdownOption{
|
||||
Mode: "gfm",
|
||||
Mode: mode,
|
||||
Text: "",
|
||||
Context: Repo,
|
||||
Wiki: true,
|
||||
|
@ -71,7 +92,22 @@ func TestAPI_RenderGFM(t *testing.T) {
|
|||
m, resp := createContext(req)
|
||||
ctx := wrap(m)
|
||||
|
||||
testCases := []string{
|
||||
options.Text = text
|
||||
web.SetForm(ctx, &options)
|
||||
Markdown(ctx)
|
||||
assert.Equal(t, responseBody, resp.Body.String())
|
||||
assert.Equal(t, responseCode, resp.Code)
|
||||
resp.Body.Reset()
|
||||
}
|
||||
|
||||
func TestAPI_RenderGFM(t *testing.T) {
|
||||
markup.Init(&markup.ProcessorHelper{
|
||||
IsUsernameMentionable: func(ctx go_context.Context, username string) bool {
|
||||
return username == "r-lyeh"
|
||||
},
|
||||
})
|
||||
|
||||
testCasesCommon := []string{
|
||||
// dear imgui wiki markdown extract: special wiki syntax
|
||||
`Wiki! Enjoy :)
|
||||
- [[Links, Language bindings, Engine bindings|Links]]
|
||||
|
@ -85,6 +121,23 @@ func TestAPI_RenderGFM(t *testing.T) {
|
|||
<li>Bezier widget (by <a href="` + AppURL + `r-lyeh" rel="nofollow">@r-lyeh</a>) <a href="https://github.com/ocornut/imgui/issues/786" rel="nofollow">https://github.com/ocornut/imgui/issues/786</a></li>
|
||||
</ul>
|
||||
`,
|
||||
// Guard wiki sidebar: special syntax
|
||||
`[[Guardfile-DSL / Configuring-Guard|Guardfile-DSL---Configuring-Guard]]`,
|
||||
// rendered
|
||||
`<p><a href="` + AppSubURL + `wiki/Guardfile-DSL---Configuring-Guard" rel="nofollow">Guardfile-DSL / Configuring-Guard</a></p>
|
||||
`,
|
||||
// special syntax
|
||||
`[[Name|Link]]`,
|
||||
// rendered
|
||||
`<p><a href="` + AppSubURL + `wiki/Link" rel="nofollow">Name</a></p>
|
||||
`,
|
||||
// empty
|
||||
``,
|
||||
// rendered
|
||||
``,
|
||||
}
|
||||
|
||||
testCasesDocument := []string{
|
||||
// wine-staging wiki home extract: special wiki syntax, images
|
||||
`## What is Wine Staging?
|
||||
**Wine Staging** on website [wine-staging.com](http://wine-staging.com).
|
||||
|
@ -103,29 +156,28 @@ Here are some links to the most important topics. You can find the full list of
|
|||
<p><a href="` + AppSubURL + `wiki/Configuration" rel="nofollow">Configuration</a>
|
||||
<a href="` + AppSubURL + `wiki/raw/images/icon-bug.png" rel="nofollow"><img src="` + AppSubURL + `wiki/raw/images/icon-bug.png" title="icon-bug.png" alt="images/icon-bug.png"/></a></p>
|
||||
`,
|
||||
// Guard wiki sidebar: special syntax
|
||||
`[[Guardfile-DSL / Configuring-Guard|Guardfile-DSL---Configuring-Guard]]`,
|
||||
// rendered
|
||||
`<p><a href="` + AppSubURL + `wiki/Guardfile-DSL---Configuring-Guard" rel="nofollow">Guardfile-DSL / Configuring-Guard</a></p>
|
||||
`,
|
||||
// special syntax
|
||||
`[[Name|Link]]`,
|
||||
// rendered
|
||||
`<p><a href="` + AppSubURL + `wiki/Link" rel="nofollow">Name</a></p>
|
||||
`,
|
||||
// empty
|
||||
``,
|
||||
// rendered
|
||||
``,
|
||||
}
|
||||
|
||||
for i := 0; i < len(testCases); i += 2 {
|
||||
options.Text = testCases[i]
|
||||
web.SetForm(ctx, &options)
|
||||
Markdown(ctx)
|
||||
assert.Equal(t, testCases[i+1], resp.Body.String())
|
||||
resp.Body.Reset()
|
||||
for i := 0; i < len(testCasesCommon); i += 2 {
|
||||
text := testCasesCommon[i]
|
||||
response := testCasesCommon[i+1]
|
||||
testRenderMarkdown(t, "gfm", text, response, http.StatusOK)
|
||||
testRenderMarkup(t, "gfm", "", text, response, http.StatusOK)
|
||||
testRenderMarkdown(t, "comment", text, response, http.StatusOK)
|
||||
testRenderMarkup(t, "comment", "", text, response, http.StatusOK)
|
||||
testRenderMarkup(t, "file", "path/test.md", text, response, http.StatusOK)
|
||||
}
|
||||
|
||||
for i := 0; i < len(testCasesDocument); i += 2 {
|
||||
text := testCasesDocument[i]
|
||||
response := testCasesDocument[i+1]
|
||||
testRenderMarkdown(t, "gfm", text, response, http.StatusOK)
|
||||
testRenderMarkup(t, "gfm", "", text, response, http.StatusOK)
|
||||
testRenderMarkup(t, "file", "path/test.md", text, response, http.StatusOK)
|
||||
}
|
||||
|
||||
testRenderMarkup(t, "file", "path/test.unknown", "## Test", "Unsupported render extension: .unknown\n", http.StatusUnprocessableEntity)
|
||||
testRenderMarkup(t, "unknown", "", "## Test", "Unknown mode: unknown\n", http.StatusUnprocessableEntity)
|
||||
}
|
||||
|
||||
var simpleCases = []string{
|
|
@ -56,6 +56,8 @@ type swaggerParameterBodies struct {
|
|||
// in:body
|
||||
EditLabelOption api.EditLabelOption
|
||||
|
||||
// in:body
|
||||
MarkupOption api.MarkupOption
|
||||
// in:body
|
||||
MarkdownOption api.MarkdownOption
|
||||
|
||||
|
|
92
routers/common/markup.go
Normal file
92
routers/common/markup.go
Normal 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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
44
routers/web/misc/markup.go
Normal file
44
routers/web/misc/markup.go
Normal 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)
|
||||
}
|
|
@ -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() {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -192,7 +192,7 @@
|
|||
<div class="ui comment form">
|
||||
<div class="ui top attached tabular menu">
|
||||
<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 class="ui bottom attached active write tab segment">
|
||||
<textarea class="review-textarea js-quick-submit" tabindex="1" name="content"></textarea>
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
<input type="hidden" name="diff_base_cid">
|
||||
<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="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 class="field">
|
||||
<div class="ui active tab" data-tab="write">
|
||||
|
|
|
@ -31,15 +31,15 @@
|
|||
<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>
|
||||
{{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>
|
||||
{{end}}
|
||||
</div>
|
||||
<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}}"
|
||||
data-url="{{.Repository.Link}}/markdown"
|
||||
data-url="{{.Repository.Link}}/markup"
|
||||
data-context="{{.RepoLink}}"
|
||||
data-markdown-file-exts="{{.MarkdownFileExts}}"
|
||||
data-previewable-extensions="{{.PreviewableExtensions}}"
|
||||
data-line-wrap-extensions="{{.LineWrapExtensions}}">
|
||||
{{.FileContent}}</textarea>
|
||||
<div class="editor-loading is-loading"></div>
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
<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="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 class="field">
|
||||
<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 -}}
|
||||
</textarea>
|
||||
</div>
|
||||
|
|
|
@ -168,7 +168,7 @@
|
|||
<div class="ui comment form">
|
||||
<div class="ui top tabular menu">
|
||||
<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 class="field">
|
||||
<div class="ui bottom active tab write">
|
||||
|
|
|
@ -53,7 +53,7 @@
|
|||
<label>{{.locale.Tr "repo.release.content"}}</label>
|
||||
<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="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 class="ui bottom active tab" data-tab="write">
|
||||
<textarea name="content">{{.content}}</textarea>
|
||||
|
|
|
@ -21,11 +21,11 @@
|
|||
</div>
|
||||
<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="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 class="field content" data-loading="{{.locale.Tr "loading"}}">
|
||||
<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 class="field">
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue