Skip to content

Commit

Permalink
Editor preview support for external renderers (#23333)
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
3 people authored Mar 24, 2023
1 parent 9e04627 commit 84daddc
Show file tree
Hide file tree
Showing 23 changed files with 389 additions and 215 deletions.
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

0 comments on commit 84daddc

Please sign in to comment.