Skip to content

Commit 84daddc

Browse files
brechtvlzeripathlunny
authored
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 <[email protected]> Co-authored-by: Lunny Xiao <[email protected]>
1 parent 9e04627 commit 84daddc

File tree

23 files changed

+389
-215
lines changed

23 files changed

+389
-215
lines changed

custom/conf/app.example.ini

-4
Original file line numberDiff line numberDiff line change
@@ -993,10 +993,6 @@ ROUTER = console
993993
;; List of file extensions for which lines should be wrapped in the Monaco editor
994994
;; Separate extensions with a comma. To line wrap files without an extension, just put a comma
995995
;LINE_WRAP_EXTENSIONS = .txt,.md,.markdown,.mdown,.mkd,
996-
;;
997-
;; Valid file modes that have a preview API associated with them, such as api/v1/markdown
998-
;; Separate the values by commas. The preview tab in edit mode won't be displayed if the file extension doesn't match
999-
;PREVIEWABLE_FILE_MODES = markdown
1000996

1001997
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
1002998
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

modules/markup/renderer.go

+13
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,11 @@ type ErrUnsupportedRenderExtension struct {
283283
Extension string
284284
}
285285

286+
func IsErrUnsupportedRenderExtension(err error) bool {
287+
_, ok := err.(ErrUnsupportedRenderExtension)
288+
return ok
289+
}
290+
286291
func (err ErrUnsupportedRenderExtension) Error() string {
287292
return fmt.Sprintf("Unsupported render extension: %s", err.Extension)
288293
}
@@ -317,3 +322,11 @@ func IsMarkupFile(name, markup string) bool {
317322
}
318323
return false
319324
}
325+
326+
func PreviewableExtensions() []string {
327+
extensions := make([]string, 0, len(extRenderers))
328+
for extension := range extRenderers {
329+
extensions = append(extensions, extension)
330+
}
331+
return extensions
332+
}

modules/setting/repository.go

+3-6
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,7 @@ var (
5353

5454
// Repository editor settings
5555
Editor struct {
56-
LineWrapExtensions []string
57-
PreviewableFileModes []string
56+
LineWrapExtensions []string
5857
} `ini:"-"`
5958

6059
// Repository upload settings
@@ -167,11 +166,9 @@ var (
167166

168167
// Repository editor settings
169168
Editor: struct {
170-
LineWrapExtensions []string
171-
PreviewableFileModes []string
169+
LineWrapExtensions []string
172170
}{
173-
LineWrapExtensions: strings.Split(".txt,.md,.markdown,.mdown,.mkd,", ","),
174-
PreviewableFileModes: []string{"markdown"},
171+
LineWrapExtensions: strings.Split(".txt,.md,.markdown,.mdown,.mkd,", ","),
175172
},
176173

177174
// Repository upload settings

modules/structs/miscellaneous.go

+29-1
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,41 @@ type SearchError struct {
1515
Error string `json:"error"`
1616
}
1717

18+
// MarkupOption markup options
19+
type MarkupOption struct {
20+
// Text markup to render
21+
//
22+
// in: body
23+
Text string
24+
// Mode to render (comment, gfm, markdown, file)
25+
//
26+
// in: body
27+
Mode string
28+
// Context to render
29+
//
30+
// in: body
31+
Context string
32+
// Is it a wiki page ?
33+
//
34+
// in: body
35+
Wiki bool
36+
// File path for detecting extension in file mode
37+
//
38+
// in: body
39+
FilePath string
40+
}
41+
42+
// MarkupRender is a rendered markup document
43+
// swagger:response MarkupRender
44+
type MarkupRender string
45+
1846
// MarkdownOption markdown options
1947
type MarkdownOption struct {
2048
// Text markdown to render
2149
//
2250
// in: body
2351
Text string
24-
// Mode to render
52+
// Mode to render (comment, gfm, markdown)
2553
//
2654
// in: body
2755
Mode string

routers/api/v1/api.go

+2
Original file line numberDiff line numberDiff line change
@@ -711,6 +711,7 @@ func Routes(ctx gocontext.Context) *web.Route {
711711
})
712712
}
713713
m.Get("/signing-key.gpg", misc.SigningKey)
714+
m.Post("/markup", bind(api.MarkupOption{}), misc.Markup)
714715
m.Post("/markdown", bind(api.MarkdownOption{}), misc.Markdown)
715716
m.Post("/markdown/raw", misc.MarkdownRaw)
716717
m.Group("/settings", func() {
@@ -1034,6 +1035,7 @@ func Routes(ctx gocontext.Context) *web.Route {
10341035
Patch(reqToken(auth_model.AccessTokenScopeRepo), reqRepoWriter(unit.TypeIssues, unit.TypePullRequests), bind(api.EditLabelOption{}), repo.EditLabel).
10351036
Delete(reqToken(auth_model.AccessTokenScopeRepo), reqRepoWriter(unit.TypeIssues, unit.TypePullRequests), repo.DeleteLabel)
10361037
})
1038+
m.Post("/markup", reqToken(auth_model.AccessTokenScopeRepo), bind(api.MarkupOption{}), misc.Markup)
10371039
m.Post("/markdown", reqToken(auth_model.AccessTokenScopeRepo), bind(api.MarkdownOption{}), misc.Markdown)
10381040
m.Post("/markdown/raw", reqToken(auth_model.AccessTokenScopeRepo), misc.MarkdownRaw)
10391041
m.Group("/milestones", func() {

routers/api/v1/misc/markdown.go renamed to routers/api/v1/misc/markup.go

+35-52
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,45 @@ package misc
55

66
import (
77
"net/http"
8-
"strings"
98

109
"code.gitea.io/gitea/modules/context"
1110
"code.gitea.io/gitea/modules/markup"
1211
"code.gitea.io/gitea/modules/markup/markdown"
13-
"code.gitea.io/gitea/modules/setting"
1412
api "code.gitea.io/gitea/modules/structs"
15-
"code.gitea.io/gitea/modules/util"
1613
"code.gitea.io/gitea/modules/web"
17-
18-
"mvdan.cc/xurls/v2"
14+
"code.gitea.io/gitea/routers/common"
1915
)
2016

17+
// Markup render markup document to HTML
18+
func Markup(ctx *context.APIContext) {
19+
// swagger:operation POST /markup miscellaneous renderMarkup
20+
// ---
21+
// summary: Render a markup document as HTML
22+
// parameters:
23+
// - name: body
24+
// in: body
25+
// schema:
26+
// "$ref": "#/definitions/MarkupOption"
27+
// consumes:
28+
// - application/json
29+
// produces:
30+
// - text/html
31+
// responses:
32+
// "200":
33+
// "$ref": "#/responses/MarkupRender"
34+
// "422":
35+
// "$ref": "#/responses/validationError"
36+
37+
form := web.GetForm(ctx).(*api.MarkupOption)
38+
39+
if ctx.HasAPIError() {
40+
ctx.Error(http.StatusUnprocessableEntity, "", ctx.GetErrMsg())
41+
return
42+
}
43+
44+
common.RenderMarkup(ctx.Context, form.Mode, form.Text, form.Context, form.FilePath, form.Wiki)
45+
}
46+
2147
// Markdown render markdown document to HTML
2248
func Markdown(ctx *context.APIContext) {
2349
// swagger:operation POST /markdown miscellaneous renderMarkdown
@@ -45,55 +71,12 @@ func Markdown(ctx *context.APIContext) {
4571
return
4672
}
4773

48-
if len(form.Text) == 0 {
49-
_, _ = ctx.Write([]byte(""))
50-
return
74+
mode := "markdown"
75+
if form.Mode == "comment" || form.Mode == "gfm" {
76+
mode = form.Mode
5177
}
5278

53-
switch form.Mode {
54-
case "comment":
55-
fallthrough
56-
case "gfm":
57-
urlPrefix := form.Context
58-
meta := map[string]string{}
59-
if !strings.HasPrefix(setting.AppSubURL+"/", urlPrefix) {
60-
// check if urlPrefix is already set to a URL
61-
linkRegex, _ := xurls.StrictMatchingScheme("https?://")
62-
m := linkRegex.FindStringIndex(urlPrefix)
63-
if m == nil {
64-
urlPrefix = util.URLJoin(setting.AppURL, form.Context)
65-
}
66-
}
67-
if ctx.Repo != nil && ctx.Repo.Repository != nil {
68-
// "gfm" = Github Flavored Markdown - set this to render as a document
69-
if form.Mode == "gfm" {
70-
meta = ctx.Repo.Repository.ComposeDocumentMetas()
71-
} else {
72-
meta = ctx.Repo.Repository.ComposeMetas()
73-
}
74-
}
75-
if form.Mode == "gfm" {
76-
meta["mode"] = "document"
77-
}
78-
79-
if err := markdown.Render(&markup.RenderContext{
80-
Ctx: ctx,
81-
URLPrefix: urlPrefix,
82-
Metas: meta,
83-
IsWiki: form.Wiki,
84-
}, strings.NewReader(form.Text), ctx.Resp); err != nil {
85-
ctx.InternalServerError(err)
86-
return
87-
}
88-
default:
89-
if err := markdown.RenderRaw(&markup.RenderContext{
90-
Ctx: ctx,
91-
URLPrefix: form.Context,
92-
}, strings.NewReader(form.Text), ctx.Resp); err != nil {
93-
ctx.InternalServerError(err)
94-
return
95-
}
96-
}
79+
common.RenderMarkup(ctx.Context, mode, form.Text, form.Context, "", form.Wiki)
9780
}
9881

9982
// MarkdownRaw render raw markdown HTML

routers/api/v1/misc/markdown_test.go renamed to routers/api/v1/misc/markup_test.go

+80-28
Original file line numberDiff line numberDiff line change
@@ -49,16 +49,37 @@ func wrap(ctx *context.Context) *context.APIContext {
4949
}
5050
}
5151

52-
func TestAPI_RenderGFM(t *testing.T) {
52+
func testRenderMarkup(t *testing.T, mode, filePath, text, responseBody string, responseCode int) {
53+
setting.AppURL = AppURL
54+
55+
options := api.MarkupOption{
56+
Mode: mode,
57+
Text: "",
58+
Context: Repo,
59+
Wiki: true,
60+
FilePath: filePath,
61+
}
62+
requrl, _ := url.Parse(util.URLJoin(AppURL, "api", "v1", "markup"))
63+
req := &http.Request{
64+
Method: "POST",
65+
URL: requrl,
66+
}
67+
m, resp := createContext(req)
68+
ctx := wrap(m)
69+
70+
options.Text = text
71+
web.SetForm(ctx, &options)
72+
Markup(ctx)
73+
assert.Equal(t, responseBody, resp.Body.String())
74+
assert.Equal(t, responseCode, resp.Code)
75+
resp.Body.Reset()
76+
}
77+
78+
func testRenderMarkdown(t *testing.T, mode, text, responseBody string, responseCode int) {
5379
setting.AppURL = AppURL
54-
markup.Init(&markup.ProcessorHelper{
55-
IsUsernameMentionable: func(ctx go_context.Context, username string) bool {
56-
return username == "r-lyeh"
57-
},
58-
})
5980

6081
options := api.MarkdownOption{
61-
Mode: "gfm",
82+
Mode: mode,
6283
Text: "",
6384
Context: Repo,
6485
Wiki: true,
@@ -71,7 +92,22 @@ func TestAPI_RenderGFM(t *testing.T) {
7192
m, resp := createContext(req)
7293
ctx := wrap(m)
7394

74-
testCases := []string{
95+
options.Text = text
96+
web.SetForm(ctx, &options)
97+
Markdown(ctx)
98+
assert.Equal(t, responseBody, resp.Body.String())
99+
assert.Equal(t, responseCode, resp.Code)
100+
resp.Body.Reset()
101+
}
102+
103+
func TestAPI_RenderGFM(t *testing.T) {
104+
markup.Init(&markup.ProcessorHelper{
105+
IsUsernameMentionable: func(ctx go_context.Context, username string) bool {
106+
return username == "r-lyeh"
107+
},
108+
})
109+
110+
testCasesCommon := []string{
75111
// dear imgui wiki markdown extract: special wiki syntax
76112
`Wiki! Enjoy :)
77113
- [[Links, Language bindings, Engine bindings|Links]]
@@ -85,6 +121,23 @@ func TestAPI_RenderGFM(t *testing.T) {
85121
<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>
86122
</ul>
87123
`,
124+
// Guard wiki sidebar: special syntax
125+
`[[Guardfile-DSL / Configuring-Guard|Guardfile-DSL---Configuring-Guard]]`,
126+
// rendered
127+
`<p><a href="` + AppSubURL + `wiki/Guardfile-DSL---Configuring-Guard" rel="nofollow">Guardfile-DSL / Configuring-Guard</a></p>
128+
`,
129+
// special syntax
130+
`[[Name|Link]]`,
131+
// rendered
132+
`<p><a href="` + AppSubURL + `wiki/Link" rel="nofollow">Name</a></p>
133+
`,
134+
// empty
135+
``,
136+
// rendered
137+
``,
138+
}
139+
140+
testCasesDocument := []string{
88141
// wine-staging wiki home extract: special wiki syntax, images
89142
`## What is Wine Staging?
90143
**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
103156
<p><a href="` + AppSubURL + `wiki/Configuration" rel="nofollow">Configuration</a>
104157
<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>
105158
`,
106-
// Guard wiki sidebar: special syntax
107-
`[[Guardfile-DSL / Configuring-Guard|Guardfile-DSL---Configuring-Guard]]`,
108-
// rendered
109-
`<p><a href="` + AppSubURL + `wiki/Guardfile-DSL---Configuring-Guard" rel="nofollow">Guardfile-DSL / Configuring-Guard</a></p>
110-
`,
111-
// special syntax
112-
`[[Name|Link]]`,
113-
// rendered
114-
`<p><a href="` + AppSubURL + `wiki/Link" rel="nofollow">Name</a></p>
115-
`,
116-
// empty
117-
``,
118-
// rendered
119-
``,
120159
}
121160

122-
for i := 0; i < len(testCases); i += 2 {
123-
options.Text = testCases[i]
124-
web.SetForm(ctx, &options)
125-
Markdown(ctx)
126-
assert.Equal(t, testCases[i+1], resp.Body.String())
127-
resp.Body.Reset()
161+
for i := 0; i < len(testCasesCommon); i += 2 {
162+
text := testCasesCommon[i]
163+
response := testCasesCommon[i+1]
164+
testRenderMarkdown(t, "gfm", text, response, http.StatusOK)
165+
testRenderMarkup(t, "gfm", "", text, response, http.StatusOK)
166+
testRenderMarkdown(t, "comment", text, response, http.StatusOK)
167+
testRenderMarkup(t, "comment", "", text, response, http.StatusOK)
168+
testRenderMarkup(t, "file", "path/test.md", text, response, http.StatusOK)
128169
}
170+
171+
for i := 0; i < len(testCasesDocument); i += 2 {
172+
text := testCasesDocument[i]
173+
response := testCasesDocument[i+1]
174+
testRenderMarkdown(t, "gfm", text, response, http.StatusOK)
175+
testRenderMarkup(t, "gfm", "", text, response, http.StatusOK)
176+
testRenderMarkup(t, "file", "path/test.md", text, response, http.StatusOK)
177+
}
178+
179+
testRenderMarkup(t, "file", "path/test.unknown", "## Test", "Unsupported render extension: .unknown\n", http.StatusUnprocessableEntity)
180+
testRenderMarkup(t, "unknown", "", "## Test", "Unknown mode: unknown\n", http.StatusUnprocessableEntity)
129181
}
130182

131183
var simpleCases = []string{

routers/api/v1/swagger/options.go

+2
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ type swaggerParameterBodies struct {
5656
// in:body
5757
EditLabelOption api.EditLabelOption
5858

59+
// in:body
60+
MarkupOption api.MarkupOption
5961
// in:body
6062
MarkdownOption api.MarkdownOption
6163

0 commit comments

Comments
 (0)