From 4c8f6dfa4ea7f8a3a120c5ba556c0ce3ae4937d3 Mon Sep 17 00:00:00 2001
From: wxiaoguang
Date: Mon, 26 Jan 2026 10:34:38 +0800
Subject: [PATCH] Support rendering OpenAPI spec (#36449)
Fix #20852
---
modules/git/tree_entry_test.go | 5 +-
modules/markup/asciicast/asciicast.go | 8 +-
modules/markup/console/console.go | 9 +--
modules/markup/csv/csv.go | 8 +-
modules/markup/external/external.go | 13 +--
modules/markup/external/openapi.go | 79 +++++++++++++++++++
modules/markup/main_test.go | 2 +
modules/markup/markdown/markdown.go | 10 +--
modules/markup/orgmode/orgmode.go | 8 +-
modules/markup/render.go | 43 ++++++----
modules/markup/renderer.go | 68 ++++++++++------
modules/setting/markup.go | 52 +++++++-----
package.json | 2 +
pnpm-lock.yaml | 40 ++++++++++
routers/api/v1/misc/markup_test.go | 4 +-
routers/common/markup.go | 11 ++-
routers/web/repo/render.go | 14 +---
routers/web/repo/view.go | 7 +-
routers/web/repo/view_file.go | 41 +++++-----
routers/web/repo/view_readme.go | 20 ++---
routers/web/repo/wiki.go | 8 +-
services/webtheme/webtheme.go | 2 +-
tests/integration/markup_external_test.go | 4 +-
web_src/css/standalone/swagger.css | 6 +-
web_src/js/markup/render-iframe.ts | 4 +-
.../js/standalone/external-render-iframe.ts | 3 +
web_src/js/standalone/swagger.ts | 28 +++++--
27 files changed, 322 insertions(+), 177 deletions(-)
create mode 100644 modules/markup/external/openapi.go
diff --git a/modules/git/tree_entry_test.go b/modules/git/tree_entry_test.go
index 8e3fb5ff99..3df6eeab68 100644
--- a/modules/git/tree_entry_test.go
+++ b/modules/git/tree_entry_test.go
@@ -22,8 +22,9 @@ func TestEntriesCustomSort(t *testing.T) {
&TreeEntry{name: "b-file", entryMode: EntryModeBlob},
}
expected := slices.Clone(entries)
- rand.Shuffle(len(entries), func(i, j int) { entries[i], entries[j] = entries[j], entries[i] })
- assert.NotEqual(t, expected, entries)
+ for slices.Equal(expected, entries) {
+ rand.Shuffle(len(entries), func(i, j int) { entries[i], entries[j] = entries[j], entries[i] })
+ }
entries.CustomSort(strings.Compare)
assert.Equal(t, expected, entries)
}
diff --git a/modules/markup/asciicast/asciicast.go b/modules/markup/asciicast/asciicast.go
index d86d61d7c4..b3af5eef09 100644
--- a/modules/markup/asciicast/asciicast.go
+++ b/modules/markup/asciicast/asciicast.go
@@ -20,14 +20,12 @@ func init() {
// See https://github.com/asciinema/asciinema/blob/develop/doc/asciicast-v2.md
type Renderer struct{}
-// Name implements markup.Renderer
func (Renderer) Name() string {
return "asciicast"
}
-// Extensions implements markup.Renderer
-func (Renderer) Extensions() []string {
- return []string{".cast"}
+func (Renderer) FileNamePatterns() []string {
+ return []string{"*.cast"}
}
const (
@@ -35,12 +33,10 @@ const (
playerSrcAttr = "data-asciinema-player-src"
)
-// SanitizerRules implements markup.Renderer
func (Renderer) SanitizerRules() []setting.MarkupSanitizerRule {
return []setting.MarkupSanitizerRule{{Element: "div", AllowAttr: playerSrcAttr}}
}
-// Render implements markup.Renderer
func (Renderer) Render(ctx *markup.RenderContext, _ io.Reader, output io.Writer) error {
rawURL := fmt.Sprintf("%s/%s/%s/raw/%s/%s",
setting.AppSubURL,
diff --git a/modules/markup/console/console.go b/modules/markup/console/console.go
index 492579b0a5..bcd6cfce77 100644
--- a/modules/markup/console/console.go
+++ b/modules/markup/console/console.go
@@ -20,29 +20,24 @@ func init() {
markup.RegisterRenderer(Renderer{})
}
-// Renderer implements markup.Renderer
type Renderer struct{}
var _ markup.RendererContentDetector = (*Renderer)(nil)
-// Name implements markup.Renderer
func (Renderer) Name() string {
return "console"
}
-// Extensions implements markup.Renderer
-func (Renderer) Extensions() []string {
- return []string{".sh-session"}
+func (Renderer) FileNamePatterns() []string {
+ return []string{"*.sh-session"}
}
-// SanitizerRules implements markup.Renderer
func (Renderer) SanitizerRules() []setting.MarkupSanitizerRule {
return []setting.MarkupSanitizerRule{
{Element: "span", AllowAttr: "class", Regexp: `^term-((fg[ix]?|bg)\d+|container)$`},
}
}
-// CanRender implements markup.RendererContentDetector
func (Renderer) CanRender(filename string, sniffedType typesniffer.SniffedType, prefetchBuf []byte) bool {
if !sniffedType.IsTextPlain() {
return false
diff --git a/modules/markup/csv/csv.go b/modules/markup/csv/csv.go
index 61977d458a..910a1518bc 100644
--- a/modules/markup/csv/csv.go
+++ b/modules/markup/csv/csv.go
@@ -20,20 +20,16 @@ func init() {
markup.RegisterRenderer(Renderer{})
}
-// Renderer implements markup.Renderer for csv files
type Renderer struct{}
-// Name implements markup.Renderer
func (Renderer) Name() string {
return "csv"
}
-// Extensions implements markup.Renderer
-func (Renderer) Extensions() []string {
- return []string{".csv", ".tsv"}
+func (Renderer) FileNamePatterns() []string {
+ return []string{"*.csv", "*.tsv"}
}
-// SanitizerRules implements markup.Renderer
func (Renderer) SanitizerRules() []setting.MarkupSanitizerRule {
return []setting.MarkupSanitizerRule{
{Element: "table", AllowAttr: "class", Regexp: `^data-table$`},
diff --git a/modules/markup/external/external.go b/modules/markup/external/external.go
index 3cbe14b86a..4d447e301a 100644
--- a/modules/markup/external/external.go
+++ b/modules/markup/external/external.go
@@ -21,10 +21,9 @@ import (
// RegisterRenderers registers all supported third part renderers according settings
func RegisterRenderers() {
+ markup.RegisterRenderer(&openAPIRenderer{})
for _, renderer := range setting.ExternalMarkupRenderers {
- if renderer.Enabled && renderer.Command != "" && len(renderer.FileExtensions) > 0 {
- markup.RegisterRenderer(&Renderer{renderer})
- }
+ markup.RegisterRenderer(&Renderer{renderer})
}
}
@@ -38,22 +37,18 @@ var (
_ markup.ExternalRenderer = (*Renderer)(nil)
)
-// Name returns the external tool name
func (p *Renderer) Name() string {
return p.MarkupName
}
-// NeedPostProcess implements markup.Renderer
func (p *Renderer) NeedPostProcess() bool {
return p.MarkupRenderer.NeedPostProcess
}
-// Extensions returns the supported extensions of the tool
-func (p *Renderer) Extensions() []string {
- return p.FileExtensions
+func (p *Renderer) FileNamePatterns() []string {
+ return p.FilePatterns
}
-// SanitizerRules implements markup.Renderer
func (p *Renderer) SanitizerRules() []setting.MarkupSanitizerRule {
return p.MarkupSanitizerRules
}
diff --git a/modules/markup/external/openapi.go b/modules/markup/external/openapi.go
new file mode 100644
index 0000000000..ac5eae53ff
--- /dev/null
+++ b/modules/markup/external/openapi.go
@@ -0,0 +1,79 @@
+// Copyright 2026 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package external
+
+import (
+ "fmt"
+ "html"
+ "io"
+
+ "code.gitea.io/gitea/modules/markup"
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/util"
+)
+
+type openAPIRenderer struct{}
+
+var (
+ _ markup.PostProcessRenderer = (*openAPIRenderer)(nil)
+ _ markup.ExternalRenderer = (*openAPIRenderer)(nil)
+)
+
+func (p *openAPIRenderer) Name() string {
+ return "openapi"
+}
+
+func (p *openAPIRenderer) NeedPostProcess() bool {
+ return false
+}
+
+func (p *openAPIRenderer) FileNamePatterns() []string {
+ return []string{
+ "openapi.yaml",
+ "openapi.yml",
+ "openapi.json",
+ "swagger.yaml",
+ "swagger.yml",
+ "swagger.json",
+ }
+}
+
+func (p *openAPIRenderer) SanitizerRules() []setting.MarkupSanitizerRule {
+ return nil
+}
+
+func (p *openAPIRenderer) GetExternalRendererOptions() (ret markup.ExternalRendererOptions) {
+ ret.SanitizerDisabled = true
+ ret.DisplayInIframe = true
+ ret.ContentSandbox = ""
+ return ret
+}
+
+func (p *openAPIRenderer) Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error {
+ content, err := util.ReadWithLimit(input, int(setting.UI.MaxDisplayFileSize))
+ if err != nil {
+ return err
+ }
+ // TODO: can extract this to a tmpl file later
+ _, err = io.WriteString(output, fmt.Sprintf(
+ `
+
+
+
+
+
+
+
+
+
+`,
+ setting.StaticURLPrefix,
+ setting.AssetVersion,
+ html.EscapeString(ctx.RenderOptions.RelativePath),
+ html.EscapeString(util.UnsafeBytesToString(content)),
+ setting.StaticURLPrefix,
+ setting.AssetVersion,
+ ))
+ return err
+}
diff --git a/modules/markup/main_test.go b/modules/markup/main_test.go
index 564f55ac11..a8dcff475d 100644
--- a/modules/markup/main_test.go
+++ b/modules/markup/main_test.go
@@ -14,5 +14,7 @@ import (
func TestMain(m *testing.M) {
setting.IsInTesting = true
markup.RenderBehaviorForTesting.DisableAdditionalAttributes = true
+ setting.Markdown.FileNamePatterns = []string{"*.md"}
+ markup.RefreshFileNamePatterns()
os.Exit(m.Run())
}
diff --git a/modules/markup/markdown/markdown.go b/modules/markup/markdown/markdown.go
index 792fb1f6d5..cca44a8774 100644
--- a/modules/markup/markdown/markdown.go
+++ b/modules/markup/markdown/markdown.go
@@ -240,30 +240,24 @@ func init() {
markup.RegisterRenderer(Renderer{})
}
-// Renderer implements markup.Renderer
type Renderer struct{}
var _ markup.PostProcessRenderer = (*Renderer)(nil)
-// Name implements markup.Renderer
func (Renderer) Name() string {
return MarkupName
}
-// NeedPostProcess implements markup.PostProcessRenderer
func (Renderer) NeedPostProcess() bool { return true }
-// Extensions implements markup.Renderer
-func (Renderer) Extensions() []string {
- return setting.Markdown.FileExtensions
+func (Renderer) FileNamePatterns() []string {
+ return setting.Markdown.FileNamePatterns
}
-// SanitizerRules implements markup.Renderer
func (Renderer) SanitizerRules() []setting.MarkupSanitizerRule {
return []setting.MarkupSanitizerRule{}
}
-// Render implements markup.Renderer
func (Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error {
return render(ctx, input, output)
}
diff --git a/modules/markup/orgmode/orgmode.go b/modules/markup/orgmode/orgmode.go
index ff2b7600eb..17d994734a 100644
--- a/modules/markup/orgmode/orgmode.go
+++ b/modules/markup/orgmode/orgmode.go
@@ -31,20 +31,16 @@ var (
_ markup.PostProcessRenderer = (*renderer)(nil)
)
-// Name implements markup.Renderer
func (renderer) Name() string {
return "orgmode"
}
-// NeedPostProcess implements markup.PostProcessRenderer
func (renderer) NeedPostProcess() bool { return true }
-// Extensions implements markup.Renderer
-func (renderer) Extensions() []string {
- return []string{".org"}
+func (renderer) FileNamePatterns() []string {
+ return []string{"*.org"}
}
-// SanitizerRules implements markup.Renderer
func (renderer) SanitizerRules() []setting.MarkupSanitizerRule {
return []setting.MarkupSanitizerRule{}
}
diff --git a/modules/markup/render.go b/modules/markup/render.go
index 96c9c5f0df..5785dc5ad5 100644
--- a/modules/markup/render.go
+++ b/modules/markup/render.go
@@ -4,6 +4,7 @@
package markup
import (
+ "bytes"
"context"
"fmt"
"html/template"
@@ -16,6 +17,7 @@ import (
"code.gitea.io/gitea/modules/htmlutil"
"code.gitea.io/gitea/modules/markup/internal"
"code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/typesniffer"
"code.gitea.io/gitea/modules/util"
"golang.org/x/sync/errgroup"
@@ -144,22 +146,29 @@ func (ctx *RenderContext) WithHelper(helper RenderHelper) *RenderContext {
return ctx
}
-// FindRendererByContext finds renderer by RenderContext
-// TODO: it should be merged with other similar functions like GetRendererByFileName, DetectMarkupTypeByFileName, etc
-func FindRendererByContext(ctx *RenderContext) (Renderer, error) {
+func (ctx *RenderContext) DetectMarkupRenderer(prefetchBuf []byte) Renderer {
if ctx.RenderOptions.MarkupType == "" && ctx.RenderOptions.RelativePath != "" {
- ctx.RenderOptions.MarkupType = DetectMarkupTypeByFileName(ctx.RenderOptions.RelativePath)
- if ctx.RenderOptions.MarkupType == "" {
- return nil, util.NewInvalidArgumentErrorf("unsupported file to render: %q", ctx.RenderOptions.RelativePath)
+ var sniffedType typesniffer.SniffedType
+ if len(prefetchBuf) > 0 {
+ sniffedType = typesniffer.DetectContentType(prefetchBuf)
}
+ ctx.RenderOptions.MarkupType = DetectRendererTypeByPrefetch(ctx.RenderOptions.RelativePath, sniffedType, prefetchBuf)
}
+ return renderers[ctx.RenderOptions.MarkupType]
+}
- renderer := renderers[ctx.RenderOptions.MarkupType]
+func (ctx *RenderContext) DetectMarkupRendererByReader(in io.Reader) (Renderer, io.Reader, error) {
+ prefetchBuf := make([]byte, 512)
+ n, err := util.ReadAtMost(in, prefetchBuf)
+ if err != nil && err != io.EOF {
+ return nil, nil, err
+ }
+ prefetchBuf = prefetchBuf[:n]
+ renderer := ctx.DetectMarkupRenderer(prefetchBuf)
if renderer == nil {
- return nil, util.NewNotExistErrorf("unsupported markup type: %q", ctx.RenderOptions.MarkupType)
+ return nil, nil, util.NewInvalidArgumentErrorf("unable to find a render")
}
-
- return renderer, nil
+ return renderer, io.MultiReader(bytes.NewReader(prefetchBuf), in), nil
}
func RendererNeedPostProcess(renderer Renderer) bool {
@@ -170,12 +179,12 @@ func RendererNeedPostProcess(renderer Renderer) bool {
}
// Render renders markup file to HTML with all specific handling stuff.
-func Render(ctx *RenderContext, input io.Reader, output io.Writer) error {
- renderer, err := FindRendererByContext(ctx)
+func Render(rctx *RenderContext, origInput io.Reader, output io.Writer) error {
+ renderer, input, err := rctx.DetectMarkupRendererByReader(origInput)
if err != nil {
return err
}
- return RenderWithRenderer(ctx, renderer, input, output)
+ return RenderWithRenderer(rctx, renderer, input, output)
}
// RenderString renders Markup string to HTML with all specific handling stuff and return string
@@ -287,12 +296,14 @@ func Init(renderHelpFuncs *RenderHelperFuncs) {
}
// since setting maybe changed extensions, this will reload all renderer extensions mapping
- extRenderers = make(map[string]Renderer)
+ fileNameRenderers = make(map[string]Renderer)
for _, renderer := range renderers {
- for _, ext := range renderer.Extensions() {
- extRenderers[strings.ToLower(ext)] = renderer
+ for _, pattern := range renderer.FileNamePatterns() {
+ fileNameRenderers[pattern] = renderer
}
}
+
+ RefreshFileNamePatterns()
}
func ComposeSimpleDocumentMetas() map[string]string {
diff --git a/modules/markup/renderer.go b/modules/markup/renderer.go
index 82e9348706..c62c28ad2a 100644
--- a/modules/markup/renderer.go
+++ b/modules/markup/renderer.go
@@ -14,8 +14,8 @@ import (
// Renderer defines an interface for rendering markup file to HTML
type Renderer interface {
- Name() string // markup format name
- Extensions() []string
+ Name() string // markup format name, also the renderer type, also the external tool name
+ FileNamePatterns() []string
SanitizerRules() []setting.MarkupSanitizerRule
Render(ctx *RenderContext, input io.Reader, output io.Writer) error
}
@@ -43,26 +43,52 @@ type RendererContentDetector interface {
}
var (
- extRenderers = make(map[string]Renderer)
- renderers = make(map[string]Renderer)
+ fileNameRenderers = make(map[string]Renderer)
+ renderers = make(map[string]Renderer)
)
// RegisterRenderer registers a new markup file renderer
func RegisterRenderer(renderer Renderer) {
+ // TODO: need to handle conflicts
renderers[renderer.Name()] = renderer
- for _, ext := range renderer.Extensions() {
- extRenderers[strings.ToLower(ext)] = renderer
+}
+
+func RefreshFileNamePatterns() {
+ // TODO: need to handle conflicts
+ fileNameRenderers = make(map[string]Renderer)
+ for _, renderer := range renderers {
+ for _, ext := range renderer.FileNamePatterns() {
+ fileNameRenderers[strings.ToLower(ext)] = renderer
+ }
}
}
-// GetRendererByFileName get renderer by filename
-func GetRendererByFileName(filename string) Renderer {
- extension := strings.ToLower(path.Ext(filename))
- return extRenderers[extension]
+func DetectRendererTypeByFilename(filename string) Renderer {
+ basename := path.Base(strings.ToLower(filename))
+ ext1 := path.Ext(basename)
+ if renderer := fileNameRenderers[basename]; renderer != nil {
+ return renderer
+ }
+ if renderer := fileNameRenderers["*"+ext1]; renderer != nil {
+ return renderer
+ }
+ if basename, ok := strings.CutSuffix(basename, ext1); ok {
+ ext2 := path.Ext(basename)
+ if renderer := fileNameRenderers["*"+ext2+ext1]; renderer != nil {
+ return renderer
+ }
+ }
+ return nil
}
-// DetectRendererType detects the markup type of the content
-func DetectRendererType(filename string, sniffedType typesniffer.SniffedType, prefetchBuf []byte) string {
+// DetectRendererTypeByPrefetch detects the markup type of the content
+func DetectRendererTypeByPrefetch(filename string, sniffedType typesniffer.SniffedType, prefetchBuf []byte) string {
+ if filename != "" {
+ byExt := DetectRendererTypeByFilename(filename)
+ if byExt != nil {
+ return byExt.Name()
+ }
+ }
for _, renderer := range renderers {
if detector, ok := renderer.(RendererContentDetector); ok && detector.CanRender(filename, sniffedType, prefetchBuf) {
return renderer.Name()
@@ -71,18 +97,12 @@ func DetectRendererType(filename string, sniffedType typesniffer.SniffedType, pr
return ""
}
-// DetectMarkupTypeByFileName returns the possible markup format type via the filename
-func DetectMarkupTypeByFileName(filename string) string {
- if parser := GetRendererByFileName(filename); parser != nil {
- return parser.Name()
- }
- return ""
-}
-
func PreviewableExtensions() []string {
- extensions := make([]string, 0, len(extRenderers))
- for extension := range extRenderers {
- extensions = append(extensions, extension)
+ exts := make([]string, 0, len(fileNameRenderers))
+ for p := range fileNameRenderers {
+ if s, ok := strings.CutPrefix(p, "*"); ok {
+ exts = append(exts, s)
+ }
}
- return extensions
+ return exts
}
diff --git a/modules/setting/markup.go b/modules/setting/markup.go
index caf0d5f8d9..921af60ff5 100644
--- a/modules/setting/markup.go
+++ b/modules/setting/markup.go
@@ -6,6 +6,7 @@ package setting
import (
"regexp"
"strings"
+ "sync"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/util"
@@ -43,22 +44,20 @@ var Markdown = struct {
RenderOptionsRepoFile MarkdownRenderOptions `ini:"-"`
CustomURLSchemes []string `ini:"CUSTOM_URL_SCHEMES"` // Actually it is a "markup" option because it is used in "post processor"
- FileExtensions []string
+ FileNamePatterns []string `ini:"-"`
EnableMath bool
MathCodeBlockDetection []string
MathCodeBlockOptions MarkdownMathCodeBlockOptions `ini:"-"`
}{
- FileExtensions: strings.Split(".md,.markdown,.mdown,.mkd,.livemd", ","),
- EnableMath: true,
+ EnableMath: true,
}
// MarkupRenderer defines the external parser configured in ini
type MarkupRenderer struct {
- Enabled bool
MarkupName string
Command string
- FileExtensions []string
+ FilePatterns []string
IsInputFile bool
NeedPostProcess bool
MarkupSanitizerRules []MarkupSanitizerRule
@@ -77,6 +76,13 @@ type MarkupSanitizerRule struct {
func loadMarkupFrom(rootCfg ConfigProvider) {
mustMapSetting(rootCfg, "markdown", &Markdown)
+
+ markdownFileExtensions := rootCfg.Section("markdown").Key("FILE_EXTENSIONS").Strings(",")
+ if len(markdownFileExtensions) == 0 || len(markdownFileExtensions) == 1 && markdownFileExtensions[0] == "" {
+ markdownFileExtensions = []string{".md", ".markdown", ".mdown", ".mkd", ".livemd"}
+ }
+ Markdown.FileNamePatterns = fileExtensionsToPatterns("markdown", markdownFileExtensions)
+
const none = "none"
const renderOptionShortIssuePattern = "short-issue-pattern"
@@ -215,21 +221,30 @@ func createMarkupSanitizerRule(name string, sec ConfigSection) (MarkupSanitizerR
return rule, true
}
-func newMarkupRenderer(name string, sec ConfigSection) {
- extensionReg := regexp.MustCompile(`\.\w`)
+var extensionReg = sync.OnceValue(func() *regexp.Regexp {
+ return regexp.MustCompile(`^(\.[-\w]+)+$`)
+})
- extensions := sec.Key("FILE_EXTENSIONS").Strings(",")
- exts := make([]string, 0, len(extensions))
+func fileExtensionsToPatterns(sectionName string, extensions []string) []string {
+ patterns := make([]string, 0, len(extensions))
for _, extension := range extensions {
- if !extensionReg.MatchString(extension) {
- log.Warn(sec.Name() + " file extension " + extension + " is invalid. Extension ignored")
+ if !extensionReg().MatchString(extension) {
+ log.Warn("Config section %s file extension %s is invalid. Extension ignored", sectionName, extension)
} else {
- exts = append(exts, extension)
+ patterns = append(patterns, "*"+extension)
}
}
+ return patterns
+}
- if len(exts) == 0 {
- log.Warn(sec.Name() + " file extension is empty, markup " + name + " ignored")
+func newMarkupRenderer(name string, sec ConfigSection) {
+ if !sec.Key("ENABLED").MustBool(false) {
+ return
+ }
+
+ fileNamePatterns := fileExtensionsToPatterns(name, sec.Key("FILE_EXTENSIONS").Strings(","))
+ if len(fileNamePatterns) == 0 {
+ log.Warn("Config section %s file extension is empty, markup render is ignored", name)
return
}
@@ -262,11 +277,10 @@ func newMarkupRenderer(name string, sec ConfigSection) {
}
ExternalMarkupRenderers = append(ExternalMarkupRenderers, &MarkupRenderer{
- Enabled: sec.Key("ENABLED").MustBool(false),
- MarkupName: name,
- FileExtensions: exts,
- Command: command,
- IsInputFile: sec.Key("IS_INPUT_FILE").MustBool(false),
+ MarkupName: name,
+ FilePatterns: fileNamePatterns,
+ Command: command,
+ IsInputFile: sec.Key("IS_INPUT_FILE").MustBool(false),
RenderContentMode: renderContentMode,
RenderContentSandbox: renderContentSandbox,
diff --git a/package.json b/package.json
index b9ede1ea09..acac860a39 100644
--- a/package.json
+++ b/package.json
@@ -36,6 +36,7 @@
"htmx.org": "2.0.8",
"idiomorph": "0.7.4",
"jquery": "4.0.0",
+ "js-yaml": "4.1.1",
"katex": "0.16.27",
"mermaid": "11.12.2",
"mini-css-extract-plugin": "2.10.0",
@@ -73,6 +74,7 @@
"@types/codemirror": "5.60.17",
"@types/dropzone": "5.7.9",
"@types/jquery": "3.5.33",
+ "@types/js-yaml": "4.0.9",
"@types/katex": "0.16.8",
"@types/pdfobject": "2.2.5",
"@types/sortablejs": "1.15.9",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 29a6dcbd70..4e39cff537 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -116,6 +116,9 @@ importers:
jquery:
specifier: 4.0.0
version: 4.0.0
+ js-yaml:
+ specifier: 4.1.1
+ version: 4.1.1
katex:
specifier: 0.16.27
version: 0.16.27
@@ -222,6 +225,9 @@ importers:
'@types/jquery':
specifier: 3.5.33
version: 3.5.33
+ '@types/js-yaml':
+ specifier: 4.0.9
+ version: 4.0.9
'@types/katex':
specifier: 0.16.8
version: 0.16.8
@@ -930,41 +936,49 @@ packages:
resolution: {integrity: sha512-qNQk0H6q1CnwS9cnvyjk9a+JN8BTbxK7K15Bb5hYfJcKTG1hfloQf6egndKauYOO0wu9ldCMPBrEP1FNIQEhaA==}
cpu: [arm64]
os: [linux]
+ libc: [glibc]
'@oxc-resolver/binding-linux-arm64-musl@11.16.4':
resolution: {integrity: sha512-wEXSaEaYxGGoVSbw0i2etjDDWcqErKr8xSkTdwATP798efsZmodUAcLYJhN0Nd4W35Oq6qAvFGHpKwFrrhpTrA==}
cpu: [arm64]
os: [linux]
+ libc: [musl]
'@oxc-resolver/binding-linux-ppc64-gnu@11.16.4':
resolution: {integrity: sha512-CUFOlpb07DVOFLoYiaTfbSBRPIhNgwc/MtlYeg3p6GJJw+kEm/vzc9lohPSjzF2MLPB5hzsJdk+L/GjrTT3UPw==}
cpu: [ppc64]
os: [linux]
+ libc: [glibc]
'@oxc-resolver/binding-linux-riscv64-gnu@11.16.4':
resolution: {integrity: sha512-d8It4AH8cN9ReK1hW6ZO4x3rMT0hB2LYH0RNidGogV9xtnjLRU+Y3MrCeClLyOSGCibmweJJAjnwB7AQ31GEhg==}
cpu: [riscv64]
os: [linux]
+ libc: [glibc]
'@oxc-resolver/binding-linux-riscv64-musl@11.16.4':
resolution: {integrity: sha512-d09dOww9iKyEHSxuOQ/Iu2aYswl0j7ExBcyy14D6lJ5ijQSP9FXcJYJsJ3yvzboO/PDEFjvRuF41f8O1skiPVg==}
cpu: [riscv64]
os: [linux]
+ libc: [musl]
'@oxc-resolver/binding-linux-s390x-gnu@11.16.4':
resolution: {integrity: sha512-lhjyGmUzTWHduZF3MkdUSEPMRIdExnhsqv8u1upX3A15epVn6YVwv4msFQPJl1x1wszkACPeDHGOtzHsITXGdw==}
cpu: [s390x]
os: [linux]
+ libc: [glibc]
'@oxc-resolver/binding-linux-x64-gnu@11.16.4':
resolution: {integrity: sha512-ZtqqiI5rzlrYBm/IMMDIg3zvvVj4WO/90Dg/zX+iA8lWaLN7K5nroXb17MQ4WhI5RqlEAgrnYDXW+hok1D9Kaw==}
cpu: [x64]
os: [linux]
+ libc: [glibc]
'@oxc-resolver/binding-linux-x64-musl@11.16.4':
resolution: {integrity: sha512-LM424h7aaKcMlqHnQWgTzO+GRNLyjcNnMpqm8SygEtFRVW693XS+XGXYvjORlmJtsyjo84ej1FMb3U2HE5eyjg==}
cpu: [x64]
os: [linux]
+ libc: [musl]
'@oxc-resolver/binding-openharmony-arm64@11.16.4':
resolution: {integrity: sha512-8w8U6A5DDWTBv3OUxSD9fNk37liZuEC5jnAc9wQRv9DeYKAXvuUtBfT09aIZ58swaci0q1WS48/CoMVEO6jdCA==}
@@ -1047,66 +1061,79 @@ packages:
resolution: {integrity: sha512-E8jKK87uOvLrrLN28jnAAAChNq5LeCd2mGgZF+fGF5D507WlG/Noct3lP/QzQ6MrqJ5BCKNwI9ipADB6jyiq2A==}
cpu: [arm]
os: [linux]
+ libc: [glibc]
'@rollup/rollup-linux-arm-musleabihf@4.56.0':
resolution: {integrity: sha512-jQosa5FMYF5Z6prEpTCCmzCXz6eKr/tCBssSmQGEeozA9tkRUty/5Vx06ibaOP9RCrW1Pvb8yp3gvZhHwTDsJw==}
cpu: [arm]
os: [linux]
+ libc: [musl]
'@rollup/rollup-linux-arm64-gnu@4.56.0':
resolution: {integrity: sha512-uQVoKkrC1KGEV6udrdVahASIsaF8h7iLG0U0W+Xn14ucFwi6uS539PsAr24IEF9/FoDtzMeeJXJIBo5RkbNWvQ==}
cpu: [arm64]
os: [linux]
+ libc: [glibc]
'@rollup/rollup-linux-arm64-musl@4.56.0':
resolution: {integrity: sha512-vLZ1yJKLxhQLFKTs42RwTwa6zkGln+bnXc8ueFGMYmBTLfNu58sl5/eXyxRa2RarTkJbXl8TKPgfS6V5ijNqEA==}
cpu: [arm64]
os: [linux]
+ libc: [musl]
'@rollup/rollup-linux-loong64-gnu@4.56.0':
resolution: {integrity: sha512-FWfHOCub564kSE3xJQLLIC/hbKqHSVxy8vY75/YHHzWvbJL7aYJkdgwD/xGfUlL5UV2SB7otapLrcCj2xnF1dg==}
cpu: [loong64]
os: [linux]
+ libc: [glibc]
'@rollup/rollup-linux-loong64-musl@4.56.0':
resolution: {integrity: sha512-z1EkujxIh7nbrKL1lmIpqFTc/sr0u8Uk0zK/qIEFldbt6EDKWFk/pxFq3gYj4Bjn3aa9eEhYRlL3H8ZbPT1xvA==}
cpu: [loong64]
os: [linux]
+ libc: [musl]
'@rollup/rollup-linux-ppc64-gnu@4.56.0':
resolution: {integrity: sha512-iNFTluqgdoQC7AIE8Q34R3AuPrJGJirj5wMUErxj22deOcY7XwZRaqYmB6ZKFHoVGqRcRd0mqO+845jAibKCkw==}
cpu: [ppc64]
os: [linux]
+ libc: [glibc]
'@rollup/rollup-linux-ppc64-musl@4.56.0':
resolution: {integrity: sha512-MtMeFVlD2LIKjp2sE2xM2slq3Zxf9zwVuw0jemsxvh1QOpHSsSzfNOTH9uYW9i1MXFxUSMmLpeVeUzoNOKBaWg==}
cpu: [ppc64]
os: [linux]
+ libc: [musl]
'@rollup/rollup-linux-riscv64-gnu@4.56.0':
resolution: {integrity: sha512-in+v6wiHdzzVhYKXIk5U74dEZHdKN9KH0Q4ANHOTvyXPG41bajYRsy7a8TPKbYPl34hU7PP7hMVHRvv/5aCSew==}
cpu: [riscv64]
os: [linux]
+ libc: [glibc]
'@rollup/rollup-linux-riscv64-musl@4.56.0':
resolution: {integrity: sha512-yni2raKHB8m9NQpI9fPVwN754mn6dHQSbDTwxdr9SE0ks38DTjLMMBjrwvB5+mXrX+C0npX0CVeCUcvvvD8CNQ==}
cpu: [riscv64]
os: [linux]
+ libc: [musl]
'@rollup/rollup-linux-s390x-gnu@4.56.0':
resolution: {integrity: sha512-zhLLJx9nQPu7wezbxt2ut+CI4YlXi68ndEve16tPc/iwoylWS9B3FxpLS2PkmfYgDQtosah07Mj9E0khc3Y+vQ==}
cpu: [s390x]
os: [linux]
+ libc: [glibc]
'@rollup/rollup-linux-x64-gnu@4.56.0':
resolution: {integrity: sha512-MVC6UDp16ZSH7x4rtuJPAEoE1RwS8N4oK9DLHy3FTEdFoUTCFVzMfJl/BVJ330C+hx8FfprA5Wqx4FhZXkj2Kw==}
cpu: [x64]
os: [linux]
+ libc: [glibc]
'@rollup/rollup-linux-x64-musl@4.56.0':
resolution: {integrity: sha512-ZhGH1eA4Qv0lxaV00azCIS1ChedK0V32952Md3FtnxSqZTBTd6tgil4nZT5cU8B+SIw3PFYkvyR4FKo2oyZIHA==}
cpu: [x64]
os: [linux]
+ libc: [musl]
'@rollup/rollup-openbsd-x64@4.56.0':
resolution: {integrity: sha512-O16XcmyDeFI9879pEcmtWvD/2nyxR9mF7Gs44lf1vGGx8Vg2DRNx11aVXBEqOQhWb92WN4z7fW/q4+2NYzCbBA==}
@@ -1325,6 +1352,9 @@ packages:
'@types/jquery@3.5.33':
resolution: {integrity: sha512-SeyVJXlCZpEki5F0ghuYe+L+PprQta6nRZqhONt9F13dWBtR/ftoaIbdRQ7cis7womE+X2LKhsDdDtkkDhJS6g==}
+ '@types/js-yaml@4.0.9':
+ resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==}
+
'@types/json-schema@7.0.15':
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
@@ -1477,41 +1507,49 @@ packages:
resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==}
cpu: [arm64]
os: [linux]
+ libc: [glibc]
'@unrs/resolver-binding-linux-arm64-musl@1.11.1':
resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==}
cpu: [arm64]
os: [linux]
+ libc: [musl]
'@unrs/resolver-binding-linux-ppc64-gnu@1.11.1':
resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==}
cpu: [ppc64]
os: [linux]
+ libc: [glibc]
'@unrs/resolver-binding-linux-riscv64-gnu@1.11.1':
resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==}
cpu: [riscv64]
os: [linux]
+ libc: [glibc]
'@unrs/resolver-binding-linux-riscv64-musl@1.11.1':
resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==}
cpu: [riscv64]
os: [linux]
+ libc: [musl]
'@unrs/resolver-binding-linux-s390x-gnu@1.11.1':
resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==}
cpu: [s390x]
os: [linux]
+ libc: [glibc]
'@unrs/resolver-binding-linux-x64-gnu@1.11.1':
resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==}
cpu: [x64]
os: [linux]
+ libc: [glibc]
'@unrs/resolver-binding-linux-x64-musl@1.11.1':
resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==}
cpu: [x64]
os: [linux]
+ libc: [musl]
'@unrs/resolver-binding-wasm32-wasi@1.11.1':
resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==}
@@ -5325,6 +5363,8 @@ snapshots:
dependencies:
'@types/sizzle': 2.3.10
+ '@types/js-yaml@4.0.9': {}
+
'@types/json-schema@7.0.15': {}
'@types/json5@0.0.29': {}
diff --git a/routers/api/v1/misc/markup_test.go b/routers/api/v1/misc/markup_test.go
index 38a1a3be9e..4d61b287ae 100644
--- a/routers/api/v1/misc/markup_test.go
+++ b/routers/api/v1/misc/markup_test.go
@@ -173,8 +173,8 @@ Here are some links to the most important topics. You can find the full list of

`, http.StatusOK)
- testRenderMarkup(t, "file", false, "path/test.unknown", "## Test", "unsupported file to render: \"path/test.unknown\"\n", http.StatusUnprocessableEntity)
- testRenderMarkup(t, "unknown", false, "", "## Test", "Unknown mode: unknown\n", http.StatusUnprocessableEntity)
+ testRenderMarkup(t, "file", false, "path/test.unknown", "## Test", "unable to find a render\n", http.StatusUnprocessableEntity)
+ testRenderMarkup(t, "unknown", false, "", "## Test", "unsupported render mode: unknown\n", http.StatusUnprocessableEntity)
}
var simpleCases = []string{
diff --git a/routers/common/markup.go b/routers/common/markup.go
index 00b2dd07c6..e189bcdecf 100644
--- a/routers/common/markup.go
+++ b/routers/common/markup.go
@@ -14,6 +14,7 @@ import (
"code.gitea.io/gitea/models/renderhelper"
"code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/httplib"
+ "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/markup/markdown"
"code.gitea.io/gitea/modules/setting"
@@ -31,7 +32,7 @@ func RenderMarkup(ctx *context.Base, ctxRepo *context.Repository, mode, text, ur
// and the urlPathContext is "/gitea/owner/repo/src/branch/features/feat-123/doc"
if mode == "" || mode == "markdown" {
- // raw markdown doesn't need any special handling
+ // raw Markdown doesn't need any special handling
baseLink := urlPathContext
if baseLink == "" {
baseLink = fmt.Sprintf("%s%s", httplib.GuessCurrentHostURL(ctx), urlPathContext)
@@ -39,7 +40,8 @@ func RenderMarkup(ctx *context.Base, ctxRepo *context.Repository, mode, text, ur
rctx := renderhelper.NewRenderContextSimpleDocument(ctx, baseLink).WithUseAbsoluteLink(true).
WithMarkupType(markdown.MarkupName)
if err := markdown.RenderRaw(rctx, strings.NewReader(text), ctx.Resp); err != nil {
- ctx.HTTPError(http.StatusInternalServerError, err.Error())
+ log.Error("RenderMarkupRaw: %v", err)
+ ctx.HTTPError(http.StatusInternalServerError, "failed to render raw markup")
}
return
}
@@ -92,7 +94,7 @@ func RenderMarkup(ctx *context.Base, ctxRepo *context.Repository, mode, text, ur
})
rctx = rctx.WithMarkupType("").WithRelativePath(filePath) // render the repo file content by its extension
default:
- ctx.HTTPError(http.StatusUnprocessableEntity, "Unknown mode: "+mode)
+ ctx.HTTPError(http.StatusUnprocessableEntity, "unsupported render mode: "+mode)
return
}
rctx = rctx.WithUseAbsoluteLink(true)
@@ -100,7 +102,8 @@ func RenderMarkup(ctx *context.Base, ctxRepo *context.Repository, mode, text, ur
if errors.Is(err, util.ErrInvalidArgument) {
ctx.HTTPError(http.StatusUnprocessableEntity, err.Error())
} else {
- ctx.HTTPError(http.StatusInternalServerError, err.Error())
+ log.Error("RenderMarkup: %v", err)
+ ctx.HTTPError(http.StatusInternalServerError, "failed to render markup")
}
return
}
diff --git a/routers/web/repo/render.go b/routers/web/repo/render.go
index d6447795bf..b1299c7047 100644
--- a/routers/web/repo/render.go
+++ b/routers/web/repo/render.go
@@ -32,24 +32,18 @@ func RenderFile(ctx *context.Context) {
return
}
- dataRc, err := blob.DataAsync()
+ blobReader, err := blob.DataAsync()
if err != nil {
ctx.ServerError("DataAsync", err)
return
}
- defer dataRc.Close()
-
- if markupType := markup.DetectMarkupTypeByFileName(blob.Name()); markupType == "" {
- http.Error(ctx.Resp, "Unsupported file type render", http.StatusBadRequest)
- return
- }
+ defer blobReader.Close()
rctx := renderhelper.NewRenderContextRepoFile(ctx, ctx.Repo.Repository, renderhelper.RepoFileOptions{
CurrentRefPath: ctx.Repo.RefTypeNameSubURL(),
CurrentTreePath: path.Dir(ctx.Repo.TreePath),
}).WithRelativePath(ctx.Repo.TreePath).WithInStandalonePage(true)
-
- renderer, err := markup.FindRendererByContext(rctx)
+ renderer, rendererInput, err := rctx.DetectMarkupRendererByReader(blobReader)
if err != nil {
http.Error(ctx.Resp, "Unable to find renderer", http.StatusBadRequest)
return
@@ -71,7 +65,7 @@ func RenderFile(ctx *context.Context) {
ctx.Resp.Header().Add("Content-Security-Policy", "frame-src 'self'")
}
- err = markup.RenderWithRenderer(rctx, renderer, dataRc, ctx.Resp)
+ err = markup.RenderWithRenderer(rctx, renderer, rendererInput, ctx.Resp)
if err != nil {
log.Error("Failed to render file %q: %v", ctx.Repo.TreePath, err)
http.Error(ctx.Resp, "Failed to render file", http.StatusInternalServerError)
diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go
index 8e85cc3278..8aeb1a0af8 100644
--- a/routers/web/repo/view.go
+++ b/routers/web/repo/view.go
@@ -151,12 +151,7 @@ func loadLatestCommitData(ctx *context.Context, latestCommit *git.Commit) bool {
return true
}
-func markupRender(ctx *context.Context, renderCtx *markup.RenderContext, input io.Reader) (escaped *charset.EscapeStatus, output template.HTML, err error) {
- renderer, err := markup.FindRendererByContext(renderCtx)
- if err != nil {
- return nil, "", err
- }
-
+func markupRenderToHTML(ctx *context.Context, renderCtx *markup.RenderContext, renderer markup.Renderer, input io.Reader) (escaped *charset.EscapeStatus, output template.HTML, err error) {
markupRd, markupWr := io.Pipe()
defer markupWr.Close()
diff --git a/routers/web/repo/view_file.go b/routers/web/repo/view_file.go
index aca5df944d..44bc8543b0 100644
--- a/routers/web/repo/view_file.go
+++ b/routers/web/repo/view_file.go
@@ -21,9 +21,7 @@ import (
"code.gitea.io/gitea/modules/git/attribute"
"code.gitea.io/gitea/modules/highlight"
"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/util"
"code.gitea.io/gitea/services/context"
issue_service "code.gitea.io/gitea/services/issue"
@@ -60,14 +58,19 @@ func prepareFileViewLfsAttrs(ctx *context.Context) (*attribute.Attributes, bool)
return attrs, true
}
-func handleFileViewRenderMarkup(ctx *context.Context, filename string, sniffedType typesniffer.SniffedType, prefetchBuf []byte, utf8Reader io.Reader) bool {
- markupType := markup.DetectMarkupTypeByFileName(filename)
- if markupType == "" {
- markupType = markup.DetectRendererType(filename, sniffedType, prefetchBuf)
- }
- if markupType == "" {
- return false
+func handleFileViewRenderMarkup(ctx *context.Context, prefetchBuf []byte, utf8Reader io.Reader) bool {
+ rctx := renderhelper.NewRenderContextRepoFile(ctx, ctx.Repo.Repository, renderhelper.RepoFileOptions{
+ CurrentRefPath: ctx.Repo.RefTypeNameSubURL(),
+ CurrentTreePath: path.Dir(ctx.Repo.TreePath),
+ }).WithRelativePath(ctx.Repo.TreePath)
+
+ renderer := rctx.DetectMarkupRenderer(prefetchBuf)
+ if renderer == nil {
+ return false // not supported markup
}
+ metas := ctx.Repo.Repository.ComposeRepoFileMetas(ctx)
+ metas["RefTypeNameSubURL"] = ctx.Repo.RefTypeNameSubURL()
+ rctx.WithMetas(metas)
ctx.Data["HasSourceRenderedToggle"] = true
@@ -75,19 +78,10 @@ func handleFileViewRenderMarkup(ctx *context.Context, filename string, sniffedTy
return false
}
- ctx.Data["MarkupType"] = markupType
- metas := ctx.Repo.Repository.ComposeRepoFileMetas(ctx)
- metas["RefTypeNameSubURL"] = ctx.Repo.RefTypeNameSubURL()
- rctx := renderhelper.NewRenderContextRepoFile(ctx, ctx.Repo.Repository, renderhelper.RepoFileOptions{
- CurrentRefPath: ctx.Repo.RefTypeNameSubURL(),
- CurrentTreePath: path.Dir(ctx.Repo.TreePath),
- }).
- WithMarkupType(markupType).
- WithRelativePath(ctx.Repo.TreePath).
- WithMetas(metas)
+ ctx.Data["MarkupType"] = rctx.RenderOptions.MarkupType
var err error
- ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRender(ctx, rctx, utf8Reader)
+ ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRenderToHTML(ctx, rctx, renderer, utf8Reader)
if err != nil {
ctx.ServerError("Render", err)
return true
@@ -95,7 +89,8 @@ func handleFileViewRenderMarkup(ctx *context.Context, filename string, sniffedTy
return true
}
-func handleFileViewRenderSource(ctx *context.Context, filename string, attrs *attribute.Attributes, fInfo *fileInfo, utf8Reader io.Reader) bool {
+func handleFileViewRenderSource(ctx *context.Context, attrs *attribute.Attributes, fInfo *fileInfo, utf8Reader io.Reader) bool {
+ filename := ctx.Repo.TreePath
if ctx.FormString("display") == "rendered" || !fInfo.st.IsRepresentableAsText() {
return false
}
@@ -246,10 +241,10 @@ func prepareFileView(ctx *context.Context, entry *git.TreeEntry) {
switch {
case fInfo.blobOrLfsSize >= setting.UI.MaxDisplayFileSize:
ctx.Data["IsFileTooLarge"] = true
- case handleFileViewRenderMarkup(ctx, entry.Name(), fInfo.st, buf, contentReader):
+ case handleFileViewRenderMarkup(ctx, buf, contentReader):
// it also sets ctx.Data["FileContent"] and more
ctx.Data["IsMarkup"] = true
- case handleFileViewRenderSource(ctx, entry.Name(), attrs, fInfo, contentReader):
+ case handleFileViewRenderSource(ctx, attrs, fInfo, contentReader):
// it also sets ctx.Data["FileContent"] and more
ctx.Data["IsDisplayingSource"] = true
case handleFileViewRenderImage(ctx, fInfo, buf):
diff --git a/routers/web/repo/view_readme.go b/routers/web/repo/view_readme.go
index f1fa5732f0..830709422e 100644
--- a/routers/web/repo/view_readme.go
+++ b/routers/web/repo/view_readme.go
@@ -18,7 +18,6 @@ import (
"code.gitea.io/gitea/modules/charset"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/context"
@@ -190,18 +189,15 @@ func prepareToRenderReadmeFile(ctx *context.Context, subfolder string, readmeFil
rd := charset.ToUTF8WithFallbackReader(io.MultiReader(bytes.NewReader(buf), dataRc), charset.ConvertOpts{})
- if markupType := markup.DetectMarkupTypeByFileName(readmeFile.Name()); markupType != "" {
+ rctx := renderhelper.NewRenderContextRepoFile(ctx, ctx.Repo.Repository, renderhelper.RepoFileOptions{
+ CurrentRefPath: ctx.Repo.RefTypeNameSubURL(),
+ CurrentTreePath: path.Dir(readmeFullPath),
+ }).WithRelativePath(readmeFullPath)
+ renderer := rctx.DetectMarkupRenderer(buf)
+ if renderer != nil {
ctx.Data["IsMarkup"] = true
- ctx.Data["MarkupType"] = markupType
-
- rctx := renderhelper.NewRenderContextRepoFile(ctx, ctx.Repo.Repository, renderhelper.RepoFileOptions{
- CurrentRefPath: ctx.Repo.RefTypeNameSubURL(),
- CurrentTreePath: path.Dir(readmeFullPath),
- }).
- WithMarkupType(markupType).
- WithRelativePath(readmeFullPath)
-
- ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRender(ctx, rctx, rd)
+ ctx.Data["MarkupType"] = rctx.RenderOptions.MarkupType
+ ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRenderToHTML(ctx, rctx, renderer, rd)
if err != nil {
log.Error("Render failed for %s in %-v: %v Falling back to rendering source", readmeFile.Name(), ctx.Repo.Repository, err)
delete(ctx.Data, "IsMarkup")
diff --git a/routers/web/repo/wiki.go b/routers/web/repo/wiki.go
index 90d95bd250..5f775efb22 100644
--- a/routers/web/repo/wiki.go
+++ b/routers/web/repo/wiki.go
@@ -10,7 +10,7 @@ import (
"io"
"net/http"
"net/url"
- "path/filepath"
+ "path"
"strings"
"code.gitea.io/gitea/models/renderhelper"
@@ -490,9 +490,9 @@ func Wiki(ctx *context.Context) {
}
wikiPath := entry.Name()
- if markup.DetectMarkupTypeByFileName(wikiPath) != markdown.MarkupName {
- ext := strings.ToUpper(filepath.Ext(wikiPath))
- ctx.Data["FormatWarning"] = ext + " rendering is not supported at the moment. Rendered as Markdown."
+ detectedRender := markup.DetectRendererTypeByFilename(wikiPath)
+ if detectedRender == nil || detectedRender.Name() != markdown.MarkupName {
+ ctx.Data["FormatWarning"] = "File extension " + path.Ext(wikiPath) + " is not supported at the moment. Rendered as Markdown."
}
// Get last change information.
lastCommit, err := wikiGitRepo.GetCommitByPath(wikiPath)
diff --git a/services/webtheme/webtheme.go b/services/webtheme/webtheme.go
index 57d63f4e07..8091c25713 100644
--- a/services/webtheme/webtheme.go
+++ b/services/webtheme/webtheme.go
@@ -140,7 +140,7 @@ func initThemes() {
setting.LogStartupProblem(1, log.ERROR, "Default theme %q is not available, please correct the '[ui].DEFAULT_THEME' setting in the config file", setting.UI.DefaultTheme)
}
}()
- cssFiles, err := public.AssetFS().ListFiles("/assets/css")
+ cssFiles, err := public.AssetFS().ListFiles("assets/css")
if err != nil {
log.Error("Failed to list themes: %v", err)
availableThemes = []*ThemeMetaInfo{defaultThemeMetaInfoByInternalName(setting.UI.DefaultTheme)}
diff --git a/tests/integration/markup_external_test.go b/tests/integration/markup_external_test.go
index b965766b5c..691ffcc62b 100644
--- a/tests/integration/markup_external_test.go
+++ b/tests/integration/markup_external_test.go
@@ -77,10 +77,10 @@ func TestExternalMarkupRenderer(t *testing.T) {
})
// above tested in-page rendering (no iframe), then we test iframe mode below
- r := markup.GetRendererByFileName("any-file.html").(*external.Renderer)
+ r := markup.DetectRendererTypeByFilename("any-file.html").(*external.Renderer)
defer test.MockVariableValue(&r.RenderContentMode, setting.RenderContentModeIframe)()
assert.True(t, r.NeedPostProcess())
- r = markup.GetRendererByFileName("any-file.no-sanitizer").(*external.Renderer)
+ r = markup.DetectRendererTypeByFilename("any-file.no-sanitizer").(*external.Renderer)
defer test.MockVariableValue(&r.RenderContentMode, setting.RenderContentModeIframe)()
assert.False(t, r.NeedPostProcess())
diff --git a/web_src/css/standalone/swagger.css b/web_src/css/standalone/swagger.css
index c32e392036..4285fc0551 100644
--- a/web_src/css/standalone/swagger.css
+++ b/web_src/css/standalone/swagger.css
@@ -29,9 +29,13 @@ body {
margin-right: 0.5rem;
}
+.swagger-spec-content {
+ display: none;
+}
+
@media (prefers-color-scheme: dark) {
body {
- background: #1e1e1e;
+ background: #14171a;
}
.swagger-ui, .swagger-back-link {
filter: invert(88%) hue-rotate(180deg);
diff --git a/web_src/js/markup/render-iframe.ts b/web_src/js/markup/render-iframe.ts
index 1bd6b3233f..1291dea4f8 100644
--- a/web_src/js/markup/render-iframe.ts
+++ b/web_src/js/markup/render-iframe.ts
@@ -9,7 +9,9 @@ async function loadRenderIframeContent(iframe: HTMLIFrameElement) {
if (!e.data?.giteaIframeCmd || e.data?.giteaIframeId !== iframe.id) return;
const cmd = e.data.giteaIframeCmd;
if (cmd === 'resize') {
- iframe.style.height = `${e.data.iframeHeight}px`;
+ // TODO: sometimes the reported iframeHeight is not the size we need, need to figure why. Example: openapi swagger.
+ // As a workaround, add some pixels here.
+ iframe.style.height = `${e.data.iframeHeight + 2}px`;
} else if (cmd === 'open-link') {
if (e.data.anchorTarget === '_blank') {
window.open(e.data.openLink, '_blank');
diff --git a/web_src/js/standalone/external-render-iframe.ts b/web_src/js/standalone/external-render-iframe.ts
index 8776710cda..dcfeb50541 100644
--- a/web_src/js/standalone/external-render-iframe.ts
+++ b/web_src/js/standalone/external-render-iframe.ts
@@ -21,6 +21,9 @@ function mainExternalRenderIframe() {
};
const updateIframeHeight = () => postIframeMsg('resize', {iframeHeight: document.documentElement.scrollHeight});
+ const resizeObserver = new ResizeObserver(() => updateIframeHeight());
+ resizeObserver.observe(window.document.documentElement);
+
updateIframeHeight();
window.addEventListener('DOMContentLoaded', updateIframeHeight);
// the easiest way to handle dynamic content changes and easy to debug, can be fine-tuned in the future
diff --git a/web_src/js/standalone/swagger.ts b/web_src/js/standalone/swagger.ts
index 62d1a56306..2b90dcfd41 100644
--- a/web_src/js/standalone/swagger.ts
+++ b/web_src/js/standalone/swagger.ts
@@ -1,18 +1,30 @@
import SwaggerUI from 'swagger-ui-dist/swagger-ui-es-bundle.js';
import 'swagger-ui-dist/swagger-ui.css';
+import {load as loadYaml} from 'js-yaml';
window.addEventListener('load', async () => {
- const url = document.querySelector('#swagger-ui')!.getAttribute('data-source')!;
- const res = await fetch(url);
- const spec = await res.json();
+ const elSwaggerUi = document.querySelector('#swagger-ui')!;
+ const url = elSwaggerUi.getAttribute('data-source')!;
+ let spec: any;
+ if (url) {
+ const res = await fetch(url);
+ spec = await res.json();
+ } else {
+ const elSpecContent = elSwaggerUi.querySelector('.swagger-spec-content')!;
+ const filename = elSpecContent.getAttribute('data-spec-filename');
+ const isJson = filename?.toLowerCase().endsWith('.json');
+ spec = isJson ? JSON.parse(elSpecContent.value) : loadYaml(elSpecContent.value);
+ }
// Make the page's protocol be at the top of the schemes list
const proto = window.location.protocol.slice(0, -1);
- spec.schemes.sort((a: string, b: string) => {
- if (a === proto) return -1;
- if (b === proto) return 1;
- return 0;
- });
+ if (spec?.schemes) {
+ spec.schemes.sort((a: string, b: string) => {
+ if (a === proto) return -1;
+ if (b === proto) return 1;
+ return 0;
+ });
+ }
SwaggerUI({
spec,