Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Editor preview support for external renderers #23333

Merged
merged 9 commits into from
Mar 24, 2023
4 changes: 0 additions & 4 deletions custom/conf/app.example.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
Expand Down
13 changes: 13 additions & 0 deletions modules/markup/renderer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down Expand Up @@ -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
}
9 changes: 3 additions & 6 deletions modules/setting/repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,7 @@ var (

// Repository editor settings
Editor struct {
LineWrapExtensions []string
PreviewableFileModes []string
LineWrapExtensions []string
} `ini:"-"`

// Repository upload settings
Expand Down Expand Up @@ -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
Expand Down
30 changes: 29 additions & 1 deletion modules/structs/miscellaneous.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions routers/api/v1/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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() {
Expand Down
87 changes: 35 additions & 52 deletions routers/api/v1/misc/markdown.go → routers/api/v1/misc/markup.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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]]
Expand All @@ -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).
Expand All @@ -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{
Expand Down
2 changes: 2 additions & 0 deletions routers/api/v1/swagger/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ type swaggerParameterBodies struct {
// in:body
EditLabelOption api.EditLabelOption

// in:body
MarkupOption api.MarkupOption
// in:body
MarkdownOption api.MarkdownOption

Expand Down
Loading