Skip to content

Commit 89ff250

Browse files
committed
Merge remote-tracking branch 'giteaofficial/main'
* giteaofficial/main: Support rendering OpenAPI spec (go-gitea#36449)
2 parents c5260ad + 4c8f6df commit 89ff250

27 files changed

Lines changed: 322 additions & 177 deletions

File tree

modules/git/tree_entry_test.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,9 @@ func TestEntriesCustomSort(t *testing.T) {
2222
&TreeEntry{name: "b-file", entryMode: EntryModeBlob},
2323
}
2424
expected := slices.Clone(entries)
25-
rand.Shuffle(len(entries), func(i, j int) { entries[i], entries[j] = entries[j], entries[i] })
26-
assert.NotEqual(t, expected, entries)
25+
for slices.Equal(expected, entries) {
26+
rand.Shuffle(len(entries), func(i, j int) { entries[i], entries[j] = entries[j], entries[i] })
27+
}
2728
entries.CustomSort(strings.Compare)
2829
assert.Equal(t, expected, entries)
2930
}

modules/markup/asciicast/asciicast.go

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,27 +20,23 @@ func init() {
2020
// See https://github.com/asciinema/asciinema/blob/develop/doc/asciicast-v2.md
2121
type Renderer struct{}
2222

23-
// Name implements markup.Renderer
2423
func (Renderer) Name() string {
2524
return "asciicast"
2625
}
2726

28-
// Extensions implements markup.Renderer
29-
func (Renderer) Extensions() []string {
30-
return []string{".cast"}
27+
func (Renderer) FileNamePatterns() []string {
28+
return []string{"*.cast"}
3129
}
3230

3331
const (
3432
playerClassName = "asciinema-player-container"
3533
playerSrcAttr = "data-asciinema-player-src"
3634
)
3735

38-
// SanitizerRules implements markup.Renderer
3936
func (Renderer) SanitizerRules() []setting.MarkupSanitizerRule {
4037
return []setting.MarkupSanitizerRule{{Element: "div", AllowAttr: playerSrcAttr}}
4138
}
4239

43-
// Render implements markup.Renderer
4440
func (Renderer) Render(ctx *markup.RenderContext, _ io.Reader, output io.Writer) error {
4541
rawURL := fmt.Sprintf("%s/%s/%s/raw/%s/%s",
4642
setting.AppSubURL,

modules/markup/console/console.go

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,29 +20,24 @@ func init() {
2020
markup.RegisterRenderer(Renderer{})
2121
}
2222

23-
// Renderer implements markup.Renderer
2423
type Renderer struct{}
2524

2625
var _ markup.RendererContentDetector = (*Renderer)(nil)
2726

28-
// Name implements markup.Renderer
2927
func (Renderer) Name() string {
3028
return "console"
3129
}
3230

33-
// Extensions implements markup.Renderer
34-
func (Renderer) Extensions() []string {
35-
return []string{".sh-session"}
31+
func (Renderer) FileNamePatterns() []string {
32+
return []string{"*.sh-session"}
3633
}
3734

38-
// SanitizerRules implements markup.Renderer
3935
func (Renderer) SanitizerRules() []setting.MarkupSanitizerRule {
4036
return []setting.MarkupSanitizerRule{
4137
{Element: "span", AllowAttr: "class", Regexp: `^term-((fg[ix]?|bg)\d+|container)$`},
4238
}
4339
}
4440

45-
// CanRender implements markup.RendererContentDetector
4641
func (Renderer) CanRender(filename string, sniffedType typesniffer.SniffedType, prefetchBuf []byte) bool {
4742
if !sniffedType.IsTextPlain() {
4843
return false

modules/markup/csv/csv.go

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,20 +20,16 @@ func init() {
2020
markup.RegisterRenderer(Renderer{})
2121
}
2222

23-
// Renderer implements markup.Renderer for csv files
2423
type Renderer struct{}
2524

26-
// Name implements markup.Renderer
2725
func (Renderer) Name() string {
2826
return "csv"
2927
}
3028

31-
// Extensions implements markup.Renderer
32-
func (Renderer) Extensions() []string {
33-
return []string{".csv", ".tsv"}
29+
func (Renderer) FileNamePatterns() []string {
30+
return []string{"*.csv", "*.tsv"}
3431
}
3532

36-
// SanitizerRules implements markup.Renderer
3733
func (Renderer) SanitizerRules() []setting.MarkupSanitizerRule {
3834
return []setting.MarkupSanitizerRule{
3935
{Element: "table", AllowAttr: "class", Regexp: `^data-table$`},

modules/markup/external/external.go

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,9 @@ import (
2121

2222
// RegisterRenderers registers all supported third part renderers according settings
2323
func RegisterRenderers() {
24+
markup.RegisterRenderer(&openAPIRenderer{})
2425
for _, renderer := range setting.ExternalMarkupRenderers {
25-
if renderer.Enabled && renderer.Command != "" && len(renderer.FileExtensions) > 0 {
26-
markup.RegisterRenderer(&Renderer{renderer})
27-
}
26+
markup.RegisterRenderer(&Renderer{renderer})
2827
}
2928
}
3029

@@ -38,22 +37,18 @@ var (
3837
_ markup.ExternalRenderer = (*Renderer)(nil)
3938
)
4039

41-
// Name returns the external tool name
4240
func (p *Renderer) Name() string {
4341
return p.MarkupName
4442
}
4543

46-
// NeedPostProcess implements markup.Renderer
4744
func (p *Renderer) NeedPostProcess() bool {
4845
return p.MarkupRenderer.NeedPostProcess
4946
}
5047

51-
// Extensions returns the supported extensions of the tool
52-
func (p *Renderer) Extensions() []string {
53-
return p.FileExtensions
48+
func (p *Renderer) FileNamePatterns() []string {
49+
return p.FilePatterns
5450
}
5551

56-
// SanitizerRules implements markup.Renderer
5752
func (p *Renderer) SanitizerRules() []setting.MarkupSanitizerRule {
5853
return p.MarkupSanitizerRules
5954
}

modules/markup/external/openapi.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
// Copyright 2026 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package external
5+
6+
import (
7+
"fmt"
8+
"html"
9+
"io"
10+
11+
"code.gitea.io/gitea/modules/markup"
12+
"code.gitea.io/gitea/modules/setting"
13+
"code.gitea.io/gitea/modules/util"
14+
)
15+
16+
type openAPIRenderer struct{}
17+
18+
var (
19+
_ markup.PostProcessRenderer = (*openAPIRenderer)(nil)
20+
_ markup.ExternalRenderer = (*openAPIRenderer)(nil)
21+
)
22+
23+
func (p *openAPIRenderer) Name() string {
24+
return "openapi"
25+
}
26+
27+
func (p *openAPIRenderer) NeedPostProcess() bool {
28+
return false
29+
}
30+
31+
func (p *openAPIRenderer) FileNamePatterns() []string {
32+
return []string{
33+
"openapi.yaml",
34+
"openapi.yml",
35+
"openapi.json",
36+
"swagger.yaml",
37+
"swagger.yml",
38+
"swagger.json",
39+
}
40+
}
41+
42+
func (p *openAPIRenderer) SanitizerRules() []setting.MarkupSanitizerRule {
43+
return nil
44+
}
45+
46+
func (p *openAPIRenderer) GetExternalRendererOptions() (ret markup.ExternalRendererOptions) {
47+
ret.SanitizerDisabled = true
48+
ret.DisplayInIframe = true
49+
ret.ContentSandbox = ""
50+
return ret
51+
}
52+
53+
func (p *openAPIRenderer) Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error {
54+
content, err := util.ReadWithLimit(input, int(setting.UI.MaxDisplayFileSize))
55+
if err != nil {
56+
return err
57+
}
58+
// TODO: can extract this to a tmpl file later
59+
_, err = io.WriteString(output, fmt.Sprintf(
60+
`<!DOCTYPE html>
61+
<html>
62+
<head>
63+
<meta name="viewport" content="width=device-width, initial-scale=1">
64+
<link rel="stylesheet" href="%s/assets/css/swagger.css?v=%s">
65+
</head>
66+
<body>
67+
<div id="swagger-ui"><textarea class="swagger-spec-content" data-spec-filename="%s">%s</textarea></div>
68+
<script src="%s/assets/js/swagger.js?v=%s"></script>
69+
</body>
70+
</html>`,
71+
setting.StaticURLPrefix,
72+
setting.AssetVersion,
73+
html.EscapeString(ctx.RenderOptions.RelativePath),
74+
html.EscapeString(util.UnsafeBytesToString(content)),
75+
setting.StaticURLPrefix,
76+
setting.AssetVersion,
77+
))
78+
return err
79+
}

modules/markup/main_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,7 @@ import (
1414
func TestMain(m *testing.M) {
1515
setting.IsInTesting = true
1616
markup.RenderBehaviorForTesting.DisableAdditionalAttributes = true
17+
setting.Markdown.FileNamePatterns = []string{"*.md"}
18+
markup.RefreshFileNamePatterns()
1719
os.Exit(m.Run())
1820
}

modules/markup/markdown/markdown.go

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -240,30 +240,24 @@ func init() {
240240
markup.RegisterRenderer(Renderer{})
241241
}
242242

243-
// Renderer implements markup.Renderer
244243
type Renderer struct{}
245244

246245
var _ markup.PostProcessRenderer = (*Renderer)(nil)
247246

248-
// Name implements markup.Renderer
249247
func (Renderer) Name() string {
250248
return MarkupName
251249
}
252250

253-
// NeedPostProcess implements markup.PostProcessRenderer
254251
func (Renderer) NeedPostProcess() bool { return true }
255252

256-
// Extensions implements markup.Renderer
257-
func (Renderer) Extensions() []string {
258-
return setting.Markdown.FileExtensions
253+
func (Renderer) FileNamePatterns() []string {
254+
return setting.Markdown.FileNamePatterns
259255
}
260256

261-
// SanitizerRules implements markup.Renderer
262257
func (Renderer) SanitizerRules() []setting.MarkupSanitizerRule {
263258
return []setting.MarkupSanitizerRule{}
264259
}
265260

266-
// Render implements markup.Renderer
267261
func (Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error {
268262
return render(ctx, input, output)
269263
}

modules/markup/orgmode/orgmode.go

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,20 +31,16 @@ var (
3131
_ markup.PostProcessRenderer = (*renderer)(nil)
3232
)
3333

34-
// Name implements markup.Renderer
3534
func (renderer) Name() string {
3635
return "orgmode"
3736
}
3837

39-
// NeedPostProcess implements markup.PostProcessRenderer
4038
func (renderer) NeedPostProcess() bool { return true }
4139

42-
// Extensions implements markup.Renderer
43-
func (renderer) Extensions() []string {
44-
return []string{".org"}
40+
func (renderer) FileNamePatterns() []string {
41+
return []string{"*.org"}
4542
}
4643

47-
// SanitizerRules implements markup.Renderer
4844
func (renderer) SanitizerRules() []setting.MarkupSanitizerRule {
4945
return []setting.MarkupSanitizerRule{}
5046
}

modules/markup/render.go

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
package markup
55

66
import (
7+
"bytes"
78
"context"
89
"fmt"
910
"html/template"
@@ -16,6 +17,7 @@ import (
1617
"code.gitea.io/gitea/modules/htmlutil"
1718
"code.gitea.io/gitea/modules/markup/internal"
1819
"code.gitea.io/gitea/modules/setting"
20+
"code.gitea.io/gitea/modules/typesniffer"
1921
"code.gitea.io/gitea/modules/util"
2022

2123
"golang.org/x/sync/errgroup"
@@ -144,22 +146,29 @@ func (ctx *RenderContext) WithHelper(helper RenderHelper) *RenderContext {
144146
return ctx
145147
}
146148

147-
// FindRendererByContext finds renderer by RenderContext
148-
// TODO: it should be merged with other similar functions like GetRendererByFileName, DetectMarkupTypeByFileName, etc
149-
func FindRendererByContext(ctx *RenderContext) (Renderer, error) {
149+
func (ctx *RenderContext) DetectMarkupRenderer(prefetchBuf []byte) Renderer {
150150
if ctx.RenderOptions.MarkupType == "" && ctx.RenderOptions.RelativePath != "" {
151-
ctx.RenderOptions.MarkupType = DetectMarkupTypeByFileName(ctx.RenderOptions.RelativePath)
152-
if ctx.RenderOptions.MarkupType == "" {
153-
return nil, util.NewInvalidArgumentErrorf("unsupported file to render: %q", ctx.RenderOptions.RelativePath)
151+
var sniffedType typesniffer.SniffedType
152+
if len(prefetchBuf) > 0 {
153+
sniffedType = typesniffer.DetectContentType(prefetchBuf)
154154
}
155+
ctx.RenderOptions.MarkupType = DetectRendererTypeByPrefetch(ctx.RenderOptions.RelativePath, sniffedType, prefetchBuf)
155156
}
157+
return renderers[ctx.RenderOptions.MarkupType]
158+
}
156159

157-
renderer := renderers[ctx.RenderOptions.MarkupType]
160+
func (ctx *RenderContext) DetectMarkupRendererByReader(in io.Reader) (Renderer, io.Reader, error) {
161+
prefetchBuf := make([]byte, 512)
162+
n, err := util.ReadAtMost(in, prefetchBuf)
163+
if err != nil && err != io.EOF {
164+
return nil, nil, err
165+
}
166+
prefetchBuf = prefetchBuf[:n]
167+
renderer := ctx.DetectMarkupRenderer(prefetchBuf)
158168
if renderer == nil {
159-
return nil, util.NewNotExistErrorf("unsupported markup type: %q", ctx.RenderOptions.MarkupType)
169+
return nil, nil, util.NewInvalidArgumentErrorf("unable to find a render")
160170
}
161-
162-
return renderer, nil
171+
return renderer, io.MultiReader(bytes.NewReader(prefetchBuf), in), nil
163172
}
164173

165174
func RendererNeedPostProcess(renderer Renderer) bool {
@@ -170,12 +179,12 @@ func RendererNeedPostProcess(renderer Renderer) bool {
170179
}
171180

172181
// Render renders markup file to HTML with all specific handling stuff.
173-
func Render(ctx *RenderContext, input io.Reader, output io.Writer) error {
174-
renderer, err := FindRendererByContext(ctx)
182+
func Render(rctx *RenderContext, origInput io.Reader, output io.Writer) error {
183+
renderer, input, err := rctx.DetectMarkupRendererByReader(origInput)
175184
if err != nil {
176185
return err
177186
}
178-
return RenderWithRenderer(ctx, renderer, input, output)
187+
return RenderWithRenderer(rctx, renderer, input, output)
179188
}
180189

181190
// RenderString renders Markup string to HTML with all specific handling stuff and return string
@@ -287,12 +296,14 @@ func Init(renderHelpFuncs *RenderHelperFuncs) {
287296
}
288297

289298
// since setting maybe changed extensions, this will reload all renderer extensions mapping
290-
extRenderers = make(map[string]Renderer)
299+
fileNameRenderers = make(map[string]Renderer)
291300
for _, renderer := range renderers {
292-
for _, ext := range renderer.Extensions() {
293-
extRenderers[strings.ToLower(ext)] = renderer
301+
for _, pattern := range renderer.FileNamePatterns() {
302+
fileNameRenderers[pattern] = renderer
294303
}
295304
}
305+
306+
RefreshFileNamePatterns()
296307
}
297308

298309
func ComposeSimpleDocumentMetas() map[string]string {

0 commit comments

Comments
 (0)